Pull to refresh
134.97

Почему нет достойных форматтеров кода для Java?

Level of difficultyEasy
Reading time13 min
Views8.4K
Original author: Jan Ouwens

Форматирование кода в Java всегда было темой обсуждения среди разработчиков. Многочисленные инструменты предлагают свои решения, но ни один из них не кажется идеальным. Так, возникает вопрос: есть ли форматтер, который действительно отвечает всем нашим требованиям?

В новом переводе от команды Spring АйО рассмотрены популярные инструменты, их плюсы и минусы, а также рассуждения на тему: может ли Java-экосистема предложить достойный форматтер?


Я не нашел ни одного форматтера для Java, который бы мне понравился. И поверьте, я искал.

Давайте обсудим, потому что я действительно хочу изменить свое мнение.

Моя история с форматтерами кода

Для начала немного предыстории. Не стесняйтесь пропустить эту часть, если хотите перейти к главному.

Когда я был подростком, мы с другом работали над проектом для хобби. Мы еще не знали о системах контроля версий, поэтому каждый день в школе обменивались дискетами с исходным кодом, который я вручную интегрировал дома. Часть этого процесса включала исправление пробелов, отступов и заглавных букв. Мой друг был неплохим программистом, но его код казался мне, как перфекционисту, неопрятным. Исправление этого давало мне ложное ощущение продуктивности, и, вероятно, именно тогда в моем сознании возникла токсичная мысль: хороший программист настолько точен и заботится о своем ремесле, что ему не нужен автоматический форматтер.

Годы спустя мне пришлось проводить код-ревью с неопытным коллегой. Я не смог читать его код, потому что он был плохо отформатирован. Я не преувеличиваю, это было действительно нечитабельно. Тогда я пришел к токсичной мысли: этот человек явно не заботится о своем ремесле. Я отправил его исправлять форматирование, и затем мы продолжили ревью.

Еще несколько лет спустя токсичная мысль укоренилась еще глубже, когда я начал работать в известной голландской компании в команде, где использование форматтера было обязательным. Форматтером по умолчанию был «плохо настроенный Eclipse», и вместо того, чтобы сохранять конфигурацию в системе контроля версий, она вручную копировалась на каждую рабочую станцию. Я спросил у коллеги, что он думает по поводу этого форматтера. Он сказал: «Мне нравится, что он последователен в читаемости». Я ответил: «Ты имеешь в виду последователен в нечитабельности». Он: «Ну… да». Иногда я пытался сделать код более читаемым вручную, но каждый раз, когда кто-то редактировал тот же файл, Eclipse с удовольствием переформатировал все обратно. Тогда я попробовал изменить конфигурацию. Все подумали, что это хорошая идея, но никто не захотел перенастраивать свой Eclipse, и на этом все закончилось.

Вот как, по моим воспоминаниям, это выглядело (преувеличено для драматического эффекта):

ExpectedException
    .when(() -> EqualsVerifier.forClass(Foo.class).suppress(Warning.NONFINAL_FIELDS)
    .withPrefabValues(List.class, Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
    Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList()).verify()).assertFailure()
    .assertMessageContains("something");

Переломный момент для меня наступил спустя несколько лет, когда я работал над проектом на Scala и познакомился с Scalafmt. Один коллега настоял на его использовании, и я неохотно согласился. Это был первый форматтер, который действительно работал и работал хорошо. Конфигурация могла храниться в git вместе с проектом, и он хорошо интегрировался со всеми инструментами. Более того, я обнаружил, что он улавливал несоответствия, которые я сам допускал в коде. Видимо, теперь я недостаточно заботился о своем ремесле! Я заметил, что он улучшил код моих коллег. И он мог бы решить ту проблему с код-ревью с помощью всего одной кнопки.

Это послужило концом моих токсичных мыслей о форматировании: мое мнение изменилось.

Я решил внедрить форматтер в EqualsVerifier. Я попробовал несколько вариантов, начал с google-java-format, перешел на Prettier Java и пришел к смелому выводу, что все инструменты форматирования для Java неудовлетворительны.

Так что же я хочу от форматтера?

В основном, я хочу то, что есть у форматтеров в других языковых экосистемах, таких как Scala, Rust и Go. А именно:

  • 🏗 Maven: Интеграция с Maven. Все, что не проверяется CI, — это лишь предложение, и может не существовать. Я хочу, чтобы CI проверял форматирование и валил билд, если оно неправильное, а Maven мог переформатировать код, чтобы участникам не нужно было ничего устанавливать. Как следствие, если нужно установить бинарный файл, это также должен делать сам Maven.

Комментарий от команды Spring АйО

Под "бинарным файлом", вероятно, принимаются форматтеры сами по себе и необходимые для них интерпретаторы или рантаймы (например, node.js для prettier-java)

  • 🏃‍♂️ Скорость: Он должен быть быстрым (или: должен быть быстрый способ форматирования файла). Таким образом, я мог бы вызвать его из Neovim. Идеальным вариантом был бы инструмент командной строки, но Maven слишком медленный. Даже mvnd недостаточно быстр для этого. В отсутствие инструмента командной строки я мог бы допустить какую-то интеграцию с Language Server Protocol. Понимаю, что для многих это не актуально, но для меня это важно, и, на мой взгляд, это вполне разумное требование: это стандартная практика для других языковых экосистем.

  • ✨ Красота: Он должен хорошо форматировать. Может показаться очевидным, но я хочу, чтобы форматированный код выглядел красиво. См. пример кода в предыдущем абзаце, чтобы увидеть, как форматтер не справляется.

  • 🚀 Эргономичность: Он не должен мешать мне. Я не хочу, чтобы мне приходилось разбираться с несовместимостями версий или преодолевать странные препятствия при настройке.

Если форматтер не справляется с одним из этих критериев, он проваливает задачу в целом. Некоторые критерии могут быть субъективными; в таких случаях я буду судьей и палачом. И опять же, все форматтеры для других языков соответствуют этим критериям, так что странно, что Java-форматтеры не могут предоставить то же самое, верно? Ведь Java — это не нишевый язык с исторически плохими инструментами.

Есть и другие факторы, которые я учитываю, но они для меня менее важны:

  • 🧠 IntelliJ: Плагин для IntelliJ был бы полезен. Сам же я не использую IntelliJ, но много кто использует его, и я хочу, чтобы для них все было просто.

  • ⚙️ Конфигурация: У некоторых форматтеров много опций, у других их нет, как у Go. Меня устраивают оба варианта (пока результат хороший), но это интересно учитывать.

Обзор форматтеров

Итак, давайте обсудим все форматтеры, о которых я знаю, один за другим.

Встроенный форматтер IntelliJ

  • 🏗 Maven: провал

  • 🏃‍♂️ Скорость: провал

  • ✨ Красота: хорошо

  • 🚀 Эргономичность: хорошо

  • 🧠 IntelliJ: отлично

  • ⚙️ Конфигурация: много опций

Это приличный форматтер, но он страдает от «синдрома Kotlin»: его нельзя использовать нигде, кроме IntelliJ. Нельзя вызвать его через Maven, и уж точно нельзя использовать из командной строки.

Кроме того, если его не настроить, он не будет работать с переносами строк. Это может быть хорошо, так как у вас появляется больше контроля над форматированием кода, но могут возникнуть различные пограничные случаи. Результаты не всегда предсказуемы, и возможны такие варианты:

EqualsVerifier.simple().forClass(Foo.class).verify();
EqualsVerifier
        .simple()
        .forClass(Foo.class)
        .verify();
EqualsVerifier.simple()
        .forClass(Foo.class).verify();

Для меня это не критично. Однако отсутствие инструментов вне IntelliJ — это серьезный недостаток.

Окончательный вердикт: провал

Пример с несогласованным переносом строк:

ExpectedException.when(
                () ->
                        EqualsVerifier.forClass(Foo.class).suppress(Warning.NONFINAL_FIELDS)
                                .withPrefabValues(
                                        List.class,
                                        Arrays.asList(1, 2, 3).stream().map(i -> i + 1)
                                                .toList(),
                                        Arrays.asList(1, 2, 3).stream()
                                                .map(i -> i + 2).toList())
                                .verify())
        .assertFailure()
        .assertMessageContains("something");

google-java-format

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: отлично

  • ✨ Красота: провал

  • 🚀 Эргономичность: отлично

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: никаких опций

Это должен был быть очевидный выбор. Интеграция с инструментами отличная, и качество форматирования было бы хорошим, если бы не одно крайне странное решение в дизайне.

Кажется, Google решили, что обсуждение пробелов и табуляций недостаточно раскрученная тематика, и добавили еще один параметр — количество пробелов. Java придерживается конвенции с 4 пробелами с 1999 года, если не раньше, и, конечно, Google решил использовать 2 пробела.

Да, в других языках используют 2 пробела. Но кого это волнует? Если вы хотите участвовать в экосистеме, вы должны следовать установленным конвенциям.

К счастью, единственная настройка, которая у них есть, позволяет использовать 4 пробела. Эта настройка называется AOSP для Android Open Source Project, потому что, когда Google приобрел его, Android использовал 4 пробела, как любая разумная Java-компания. Однако Google не документирует эту настройку на своем сайте.

Тем не менее, настройка AOSP действительно подчеркивает единственный другой недостаток google-java-format: он любит двойные, а иногда и четверные отступы, особенно с вложенными лямбдами и выражениями. Это действительно сдвигает много кода вправо в редакторе, после чего код начинает выглядеть не очень презентабельно, как вы можете увидеть в приведенных ниже примерах.

Окончательный вердикт: провал

Пример с настройкой по умолчанию — обратите внимание, что здесь нет отступов в 2 пробела; они появляются только после {:

ExpectedException.when(
        () ->
            EqualsVerifier.forClass(Foo.class)
                .suppress(Warning.NONFINAL_FIELDS)
                .withPrefabValues(
                    List.class,
                    Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
                    Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList())
                .verify())
    .assertFailure()
    .assertMessageContains("something");

Пример с AOSP — обратите внимание, как все смещается вправо:

ExpectedException.when(
                () ->
                        EqualsVerifier.forClass(Foo.class)
                                .suppress(Warning.NONFINAL_FIELDS)
                                .withPrefabValues(
                                        List.class,
                                        Arrays.asList(1, 2, 3).stream()
                                                .map(i -> i + 1)
                                                .toList(),
                                        Arrays.asList(1, 2, 3).stream()
                                                .map(i -> i + 2)
                                                .toList())
                                .verify())
        .assertFailure()
        .assertMessageContains("something");

Prettier Java

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: хорошо

  • ✨ Красота: отлично

  • 🚀 Эргономичность: провал

  • 🧠 IntelliJ: провал

  • ⚙️ Конфигурация: несколько опций

Prettier Java, на первый взгляд, также очень хороший форматтер. У него разумные настройки по умолчанию, и он изначально создает красивый код. Я использую его для EqualsVerifier.

Однако у него есть много побочных проблем:

  • Это расширение для Prettier, форматтера для JavaScript. Это значит, что для его работы требуется полный NodeJS, что, мягко говоря, не очень удобно.

  • Его установка может быть довольно сложной, потому что он устанавливается как плагин к Prettier. Если вы хотите использовать prettierd, чтобы ускорить процесс, установка станет еще сложнее.

  • Форматирование нестабильно между версиями Prettier Java, так что вам, возможно, придется переформатировать весь код при каждом обновлении.

  • Из-за этого вы можете захотеть привязать свою версию Prettier Java к конкретной версии, что делает управление установкой prettierd еще сложнее, потому что теперь вы не можете обновлять систему вслепую. Если версии Prettier Java в вашем скрипте сборки и редакторе расходятся, они начнут конфликтовать друг с другом, что не очень весело.

  • Нет плагина для IntelliJ.

Окончательный вердикт: провал

Пример с Prettier Java 2.4.0:

ExpectedException
    .when(() ->
        EqualsVerifier
            .forClass(Foo.class)
            .suppress(Warning.NONFINAL_FIELDS)
            .withPrefabValues(
                List.class,
                Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
                Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList()
            )
            .verify()
    )
    .assertFailure()
    .assertMessageContains("something");

Пример с Prettier Java 2.6.0 — обратите внимание, как () -> получает отдельную строку, а .when и .forClass — нет, и что происходит с этой одинокой скобкой )!?

ExpectedException.when(
    () ->
        EqualsVerifier.forClass(Foo.class)
            .suppress(Warning.NONFINAL_FIELDS)
            .withPrefabValues(
                List.class,
                Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
                Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList()
            )
            .verify()
)
    .assertFailure()
    .assertMessageContains("something");

Форматтер Eclipse JDT

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: так себе

  • ✨ Красота: хорошо

  • 🚀 Эргономичность: провал

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: много опций

Форматтер, который в своё время отбил у меня желание пользоваться форматтерами. Но если его правильно настроить, он на самом деле очень неплох.

И вот тут-то и возникает проблема. Хотя этот форматтер можно использовать как более-менее автономный инструмент, в отличие от форматтера IntelliJ, он всё же сильно интегрирован с IDE Eclipse, и его нужно запускать для настройки. А его нужно настраивать. Вы можете экспортировать настройки в XML-файл такой структуры, что вам не захочется редактировать его вручную. Иными словами, вы не сможете использовать этот форматтер без установки Eclipse, что… ну сами понимаете.

Во время исследования для этой статьи я нашел, что IntelliJ умеет импортировать и экспортировать конфигурации форматтера Eclipse. Однако я не знаю, насколько хорошо это работает.

Также, несмотря на то, что существует два разных плагина для Maven, позволяющих запускать этот форматтер, нет нормального инструмента командной строки. Лазейка в том, что LSP, который я использую для Java, основан на Eclipse и имеет встроенный форматтер, так что я всё равно могу запускать его из Vim. Но если я когда-нибудь захочу попробовать новый LSP от Oracle, у меня снова не будет форматтера.

Поскольку форматтер Eclipse JDT — это, по сути, библиотека Java, которую можно использовать в различных инструментах, не должно быть сложно создать консольную утилиту на основе GraalVM для его вызова. Но это добавляет много сложностей при его использовании, которые уже были из-за необходимости настройки через Eclipse.

Окончательный вердикт: провал

Пример без настроек:

ExpectedException
    .when(() -> EqualsVerifier.forClass(Foo.class).suppress(Warning.NONFINAL_FIELDS)
        .withPrefabValues(List.class, Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
            Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList())
        .verify())
    .assertFailure().assertMessageContains("something");

Пример с настройками:

  • Wrapping settings → Function calls → Arguments → “Wrap all elements, except first element if not necessary”

  • Wrapping settings → Function calls → Qualified invocations → “Wrap all elements, every element on a new line”

ExpectedException
  .when(() -> EqualsVerifier
      .forClass(Foo.class)
      .suppress(Warning.NONFINAL_FIELDS)
      .withPrefabValues(List.class,
          Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
          Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList())
      .verify())
  .assertFailure()
  .assertMessageContains("something");

Palantir Java Format

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: провал

  • ✨ Красота: отлично

  • 🚀 Эргономичность: хорошо

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: никаких опций

Palantir Java Format основан на google-java-format, и они как-то умудряются сделать плохие стороны Google хорошими, а хорошие — плохими, и все это одновременно.

С одной стороны, они делают ужасное форматирование google-java-format намного лучше. Но при этом не предоставляют инструмент командной строки. Да, есть интеграция с Maven, и да, есть плагин для IntelliJ. Но никакого официального инструмента командной строки. Странно.

Есть способ обойти это. Если вы используете Arch, вы можете скачать скрипт из AUR, который можно запустить из командной строки, и, конечно, вы можете легко адаптировать этот скрипт под другую операционную систему. Придется как-то собрать и упаковать зависимости. Если вы хотите пойти по этому пути, вот обновленные оценки:

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: хорошо

  • ✨ Красота: отлично

  • 🚀 Эргономичность: провал

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: никаких опций

Palantir проваливает эргономику по той причине, что вам придется выяснить, как делиться скриптом и его зависимостями с другими людьми, и как их обновлять.

Этот скрипт показывает, что Palantir, вероятно, является оберткой для google-java-format, а не его форком. Я не знаю, что это значит для долгосрочной стабильности проекта, и не знаю, стоит ли мне об этом беспокоиться.

Кроме того, есть еще деловые вопросы компании Palantir, стоящей за этим форматтером. Я не знаю, стоит ли мне об этом беспокоиться. Но, возможно, стоит. Думаю, да.

В любом случае, беспорядок с командной строкой — это для меня ключевой недостаток.

Окончательный вердикт: провал

Пример:

ExpectedException.when(() -> EqualsVerifier.forClass(Foo.class)
                .suppress(Warning.NONFINAL_FIELDS)
                .withPrefabValues(
                        List.class,
                        Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
                        Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList())
                .verify())
        .assertFailure()
        .assertMessageContains("something");

Spring Java Format

  • 🏗 Maven: хорошо

  • 🏃‍♂️ Скорость: провал

  • ✨ Красота: хорошо

  • 🚀 Эргономичность: провал

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: никаких опций

Кажется, это обертка над Eclipse JDT с захардкоженой конфигурацией. Форматирование выглядит достаточно хорошо, и есть плагины для всех инструментов сборки и IDE… но нет инструмента командной строки.

По какой-то причине требуется модификация файла .m2/settings.xml, что странно.

Окончательный вердикт: провал

Пример:

ExpectedException
    .when(() ->
        EqualsVerifier
            .forClass(Foo.class)
            .suppress(Warning.NONFINAL_FIELDS)
            .withPrefabValues(
                List.class,
                Arrays.asList(1, 2, 3).stream().map(i -> i + 1).toList(),
                Arrays.asList(1, 2, 3).stream().map(i -> i + 2).toList()
            )
            .verify()
    )
    .assertFailure()
    .assertMessageContains("something");

Форматтеры для разных языков

  • 🏗 Maven: провал

  • 🏃‍♂️ Скорость: отлично

  • ✨ Красота: варьируется

  • 🚀 Эргономичность: провал

  • 🧠 IntelliJ: хорошо

  • ⚙️ Конфигурация: много опций

Есть много языков, которые выглядят как Java (потому что все они произошли от C), поэтому логично, что существуют различные инструменты, которые могут форматировать все эти языки: ClangFormat, Artistic Style (astyle), Uncrustify

Они все имеют схожесть в том, что у них отличная поддержка командной строки. У некоторых есть плагины для IntelliJ, у других нет, но всем им сложно пользоваться через Maven: их нужно предварительно устанавливать и использовать exec-maven-plugin для их запуска.

Кроме того, поскольку существует так много вариантов конфигурации, а инструменты ориентированы на другие языки, кроме Java, они не выглядят особенно хорошо без дополнительных настроек, поэтому будьте готовы потратить много времени на доработку. Именно по этой причине я не буду приводить примеры, так как без настроек они мало что значат.

Окончательный вердикт: провал

EditorConfig

Для полноты картины упомяну EditorConfig. Это хороший независимый от редактора и языка инструмент, который обеспечивает единообразное использование табуляций/пробелов, символов конца строки и отступов. Но не более того.

Это очень хороший и полезный инструмент, который хорошо справляется со своей задачей, но так как это не полноценный форматтер, я не буду его оценивать.

Spotless

Я также упомяну Spotless, так как многие люди на Reddit говорят о нем, когда их спрашивают, каким форматтером для Java они пользуются.

Spotless не является форматтером. Это плагин для Maven и Gradle, который можно использовать для запуска большинства вышеупомянутых форматтеров, и он делает это очень хорошо!

Итоги

Если я соберу все окончательные вердикты для всех форматтеров, о которых я говорил, получается очевидная тенденция:

  • Встроенный форматтер IntelliJ: провал

  • google-java-format: провал

  • Prettier Java: провал

  • Eclipse JDT: провал

  • Palantir Java Format: провал

  • Spring Java Format: провал

  • Форматтеры для разных языков: провал

Ни один из форматтеров, которые я нашел, не удовлетворяет всем заявленным требованиям.

Это делает экосистему Java очень отличной от экосистем большинства других (современных) языков, где есть единый канонический инструмент, который удовлетворяет всем требованиям.

Почему в Java так не получается?

Завершение

В данный момент я использую Prettier Java для EqualsVerifier, но я очень открыт для миграции на другой инструмент. Однако я затрудняюсь в выборе, потому что у всех есть недостатки, которые мне не нравятся. Я уже менял форматтеры (сначала с ничего на google-java-format, а затем с google-java-format на Prettier Java), поэтому если я снова соберусь что-то поменять, то хотел бы поменять на что-то действительно хорошее, а такого просто нет.

С другой стороны, некая инерция заставляет меня оставаться с Prettier Java, который вызывает раздражающие проблемы при каждом обновлении.

А также есть загрязнение истории Git, которое происходит, когда вы меняете форматтеры (или обновляете Prettier Java). К счастью, у Git есть умный способ справляться с этим: вы можете добавить файл с хэшами коммитов Git в свой репозиторий, и тогда Git сможет игнорировать эти коммиты при выполнении git blame. Подробнее об этом можно прочитать здесь.

Заключение

Если вам не нужно запускать форматтер из командной строки, и вас устраивает его этичность, я думаю, что Palantir, вероятно, ваш лучший вариант. В противном случае, я не могу рекомендовать ни один форматтер на данный момент.

Может быть, я упустил какой-то форматтер? Или есть какая-то конфигурация или функция, которая могла бы изменить мою оценку форматтера? Пожалуйста, дайте знать!

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Tags:
Hubs:
Total votes 29: ↑22 and ↓7+23
Comments21

Articles

Information

Website
t.me
Registered
Employees
11–30 employees