Автоматический рефакторинг кода с помощью OpenRewrite
Попробовал применить OpenRewrite к репозиторию Gradle и если вкратце, то все очень плохо.
В официальной документации очень солидные заявления — утилита позволяет делать автоматический рефакторинг на основе рецептов, при этом код преобразуется в семантические деревья, к ним применяются трансформации и результат “минимально инвазивно” записывается обратно. Основная фишка инструмента, как я понял, заключается в том, что вы пишете правила для рефакторинга (или используете готовые), а потом применяете скопом к своей большой кодовой базе (возможно из нескольких репозиториев), экономя время на ручной труд.
Но реальность полна разочарований.
Подключение к проекту и запуск
Довольно быстро обнаружилось, что плагин для Gradle не поддерживает современные фишки: кэш настроек, параллельное выполнение (с весьма странным багом) и т.п.
Вдобавок к этому, парсер давился на Groovy файлах (у нас большинство тестов написаны на нем), что довольно странно, ведь вроде инструмент поддерживает Groovy.
Но это еще цветочки, эта приблуда еще пыталась обработать jsp из ресурсов!
Т.е. она даже не понимает, где исходники и на каком языке, и видимо молотит все текстовые файлы подряд.
Через час (sic!) после запуска я получил… OOM. Возможно где-то есть утечка памяти, либо в самом Gradle, либо в плагине. Я в итоге плюнул и написал скрипт, который применяет утилиту к одному подпроекту за раз. Память пришлось поднять только для одного большого подпроекта.
Работает утилита, мягко говоря, не быстро: чтобы весь код прошерстить, ей понадобилось больше 2 часов (и это на топовом маке). Впрочем, насчет производительности разработчики вряд ли парятся — платный вариант как раз обещает ускорение за счет своей БД, чтобы не перестраивать семантические деревья каждый раз с нуля.
Сами изменения
Сначала я попробовал применить рецепт по миграции на Java 8 — первая версия Gradle была написана аж 2008, кода довольно много, и что-то могло остаться в “старом стиле”. Но этот рецепт никаких изменений не внес.
Далее я попробовал миграцию на Java 17 — там уже была куча правок, но не стал углубляться, т.к. ее обновление пока только запланировано.
Пока я ждал первого прогона, почитал документацию в целом и поискал, какие есть интересные готовые рецепты.
Инструмент очень сильно сфокусирован на Java (даже не JVM).
Кроме обновления версии Java я нашел только 2 интересных рецепта: CodeCleanup и CommonStaticAnalysis.
Там были и другие, например для обновления билд-скриптов, миграции на новую версию Hibernate и т.п., но большинство из них были довольно узко применимыми.
Сами изменения были не без ошибок:
- добавлялся
finalк свойствам, которые потом изменялись, и к классам, у которых были наследники; - удалялись/добавлялись параметры типов там где не надо;
- была добавлена куча пустых конструкторов (видимо, чтобы сузить область видимости), но ценности я в этом не увидел;
- пока не добавил исключения, Groovy файлы были обновлены по непонятным правилам: например,
as SomeClassпревратилось вasSomeClass.
Отдельно отмечу упрощение условий: оно было настолько тупое, что заменило
!(methodName != null ? !methodName.equals(that.methodName) : that.methodName != null);
на
methodName == null ? !methodName.equals(that.methodName) : that.methodName != null;
что буквально вызовет NPE.
Тут у меня начались вьетнамские флешбеки от кривых #define в Си, где надо было огораживать все скобками, иначе получится шляпа.
При этом IntelliJ через пару Alt+Enter заменит этот код на человеческий
Objects.equals(methodName, that.methodName)
Другой показательный пример:
public boolean isEmpty() {
return size() == 0;
}
был заменен на
public boolean isEmpty() {
return isEmpty();
}
Да-да, это классическая реализация StackOverflowError.
Все вышеперечисленное приводило к ошибкам компиляции и к ошибкам от существующих средств проверки качества кода (-Werror, ErrorProne, CodeNarc и т.п.).
Из-за этого я переписал скрипт, чтобы после применения рецептов к подпроекту запускалась компиляция.
Разумеется, это еще больше замедлило и без того небыстрый процесс.
И ошибки компиляции править приходилось вручную, так что продуктивность полетела прямиком в помойку.
Самое отстойное, что некоторые рецепты проще всего было бы отключить совсем, но такой опции у инструмента нет. *звуки грустного тромбона*
Вывод
Я был довольно упертым и все-таки применил рецепты ко всем проектам. В итоге было обновлено 1700+ файлов. Было ли там что-то такое, ради чего стоило проходить через эти пляски? Если кратко, то нет.
Были изменения в форматировании (местами сомнительные, типа Йода-условий или порядка модификаторов), манипуляции с импортами, упрощение условий (см. пример выше), везде была добавлена куча final (даже там где не надо), лямбды были заменены на ссылки на методы, убраны ненужные параметры типов, мелкие изменения типа замены .size == 0 на .isEmpty() или страшной восьмеричной системы с 0777 на понятную десятеричную с 511.
Если ваша кодовая база маленькая, то вы вряд ли будете будете писать новые рецепты — проще руками через IDE все мигрировать. Если кодовая база большая, и есть платформенная команда, то скорее всего код и так уже будет довольно однородный и кучу вещей можно будет решить скриптом/простыми текстовыми преобразованиями. Кажется, рецепты имеет смысл писать только для чего-то специфичного. Самая потенциально полезная часть инструмента — общеприменимые рецепты (типа миграции Java), но их не очень много и покрывают они на удивление мало.
Но все это просто уничтожается качеством инструмента.
От чего-то, основанного на семантическом представлении, которое строится тыщу лет, я ожидаю, что итоговый код будет хотя бы компилироваться и не содержать очевидных ошибок.
А по факту все это не сильно лучше текстовых замен или #define в Си со всеми их недостатками.
IntelliJ гораздо лучше понимает код и его контекст.
И такое ощущение, что с ней сделать те же операции было бы быстрее.
В общем, great idea, does not work.
P.S. Затеял я это все для того, чтобы посмотреть, какие есть инструменты для миграции кодовой базы с одного языка на другой. ChatGPT и прочие AI штуки сразу отмел, т.к. хотя с маленькими кусочками он и хорошо справлялся, но я думал, что недостаточно надежно будет его применять для большого количества кода. Но кажется для этой задачи он явно лучше этого изделия. Еще находил Txl, но он показался слишком сложным. Планирую попробовать его в будущем.
Сапер для вымышленной консоли на Nim
На выходных запилил игрулю, вариацию Сапёра.

Поиграть можно тут (с компа стрелочками удобнее, но на мобилах тоже работает), а исходники почитать — тут.
Про саму игру
Идея пришла в голову не слишком случайно. Недавно поставил себе классического сапера на планшет и залипал в него. Но с “аккордеонами” (которые рекурсивно открывают соседние ячейки если все однозначно) игра превращается в тупейший кликер на скорость. Прям думать-думать надо раза 2-3 за игру на “Профессионале”, и обычно после этого приходится делать случайный выбор.
“Ездить” по полю было намеренным решением. Кликать скучно, а так себя можно представить роботом, который уволился из доставки и пошел в саперы. В голове у меня застряла прочная ассоциация с Bomberman, в который тоже недавно играл, — там тоже кто-то ходит и связано со взрывами, но общего тут только ходьба по большому счету.
Получившаяся игра чисто случайно (потому что консоль 160x160 пикселей) получилась с такими же параметрами как у “Любителя”, однако она тяжелее. Тут надо думать чаще, да еще и помнить, где мины стоят (отсутствие флажков тоже было намеренным). И следить за тем, чтобы случайно не уехать дальше, чем нужно: чаще всего я проигрываю как раз из-за этого.
Вообще я заметил, что и в обычной жизни некоторые вещи хочу делать побыстрее и без особых раздумий. В результате делаю тупейшие ошибки и выходит дольше и хуже:) Все как в поговорке. Возможно, эта игра потренирует мою усидчивость.
Консоль и ее ограничения
На платформу я наткнулся, когда что-то искал для Zig в рамках своего предыдущего приключения:
там есть еще и другие языки, чтобы на них пострадать
В целом, программировать под нее оказалось довольно просто. При этом она не идеальна (я нашел пару багов, придумал фичу и даже сделал мини-пуллреквест), но компенсирует это “ламповостью”.
И наличие ограничений тут сыграло на руку: я очень быстро выкинул несколько идей (некоторые, правда были неплохие), которые могли бы затянуть разработку на недельку, и смог быстро получить что-то рабочее.
Nim
Nim я выбрал по принципу “что-то новое, но при этом хотя бы слышал” (оказывается, для этого баззворд даже есть). Про него до этого я знал только, что да, вот есть такой язык, похож на питон чем-то, но при этом системный, и вроде хвалят.
Писать на Nim было легко. Я опять поленился и не почитал даже простейшие туториалы (1, 2), но первую играбельную версию написал без проблем, просто используя те конструкции, которые казались логичными для питона/сей и при взгляде на код рядышком из шаблона. Углубиться в язык понадобилось только когда проверку решаемости добавлял.
С точки зрения синтаксиса — да, напоминает питон с его значимыми пробелами и некоторыми особенностями синтаксиса, и еще в чем-то есть сходство с F#.
Есть непривычные вещи, например, конкатенация строк через & вместо + или перевод в строку с помощью доллара ($someInt).
Забавно, что лямбды — это сахар, и для x => x + 1 надо буквально импортировать std/sugar.
Немного расстроили структуры, там обязательно надо указывать имена полей при создании, Cell(x: p.x + 1, y: y) мне не очень понравилось и я в итоге оставил просто две переменных во многих местах.
Switch не такой мощный, как хотелось бы (даже Kotlin’овский when поинтереснее), но это я по скаловскому скучаю.
Впрочем, это все скорее вкусовщина.
Одна из крутых фич — все методы это по сути расширения.
Т.е. x.pos(y) это сахар для pos(x, y).
С точки зрения программиста на Cях со структурами, да и с точки зрения ФП-шника с ворохом DTO — очень удобно.
Еще крутая фишка — оптимизированные множества.
Стандартный set — это битовый массив.
Итераторы с yield — мое почтение, тоже очень удобно.
Есть и всякие плюшки для времени компиляции, например when или static: assert, но чтобы их по настоящему оценить, нужно видимо что-то мультиплатформенное делать.
Не понравилось, что объявление функции должно быть обязательно раньше его использования: не очень дружелюбно к косвенной рекурсии, да и организовать код в файле чуть-чуть тяжелее. Возможно, надо было разбивать код на модули, но я решил, что оно того не особо стоит: весь код занимает меньше 300 строк.
Прям неожиданно споткнулся об интервалы: они паскалевские/котлиновские, т.е. вместо привычного for i in 0..n надо писать for i in 0..<n.
С одной стороны, более явно, чем “в устоявшемся” стиле, с другой — тяжело обнаружить классическую ошибку на единицу.
В этом плане отстойно, что не было никакой проверки на выход за границы массива.
И еще один бесючий момент — жесткая сегрегация численных типов без неявной конверсии между ними, т.е. int, uint8, uint32 и т.п.
Подобное было уже с Rust и Zig, и вроде в Nim чуть лучше, но все равно не нравится.
Справедливости ради, один потенциальный баг может быть за счет этого был обнаружен.
С точки зрения инструментов, видимо, это модный тренд: делать менеджер пакетов, совмещенный с инструментом сборки. Да еще чтобы скрипты сборки были на этом языке (хотя по факту это мало отличается от Make с набором альясов для баш-команд).
В целом в Nim есть интересные фичи (но и сомнительные тоже), и ощущения после него остались приятные.
Разработка
На все-про-все у меня ушло часов 10-11, и большую часть времени я потратил на установку рабочего окружения. Причем дважды: локально, чтобы не curl-sh (сесурити!) и в GitHub Actions (благословлен пусть будет act, без него было бы еще дольше).
Первую играбельную версию я получил всего через 2-3 часа после настройки окружения.
Потерял немного времени на диагностику проблемы с рандомом, чтобы в итоге использовать трюк со счетчиком фреймов.
Обнаружил еще, что функция обновления фрейма не блокирует последующий вызов, но это тоже стоило времени — для меня блокировка была как что-то само собой разумеющееся. А неправильная обработка этой ситуации вызывала проблемы с обработкой нажатий клавиш.
Наконец, подобрать более-менее пристойную картинку для логотипа тоже заняло время: найти AI-сервис генерации без регистрации и смс, выяснить, что с текстом все еще есть проблемы у старых моделей, а потом еще отмасштабировать картинку и с помощью ImageMagick снизить количество цветов до 4. Ну и поиграться с цветами, разумеется.
Из собственно алгоритмических вещей самой трудозатратной задачей было обеспечение того, что в игру можно выиграть. В ранних версиях этой проверки не было, и была возможна ситуация, когда пустое место в углу было перегорожено двумя минами. Я пытался придумать что-нибудь умное, чтобы влезть в “почти” константную память, но в итоге забил и оставил простой BFS. И была еще пара экспериментов, от которых я отказался.
Был еще забавный момент, когда я тестировал небольшое изменение, связанное с обновлением состояния игры. Я спешил пройти уровень и постоянно делал мелкие ошибки, и в итоге долго не мог отладить поведение просто потому, что не мог выиграть :)
Итого
В целом у меня остались вполне позитивные впечатления от платформы и от Nim. Игрулей я доволен, и особенно я доволен тем, что очень быстро получилось что-то сделать и поиграть.
Ограничения и предохранители
Зацепились языками с одним из коллег на тему удобства языка для разработки без IDE. Он заявил, что код на Kotlin невозможно читать без IDE. Вполне органично дискуссия перешла в плоскость того, полезны ли ограничения языка или нет.
Началось все с тезиса про то, что структура файлов в Java (1 файл на 1 класс, 1 папка часть пакета) это полезно и позволяет находить нужный класс быстрее. А вот в Kotlin можно как угодно делать. Поэтому ограничения — это хорошо, они приводят к порядку и простоте. В качестве дополнительных примеров были модификаторы видимости (C против Java), владение (C vs Rust) и т.п. Да и вообще, ограничения формируют/способствуют культуре разработки.
Когда я пытался аргументировать, что ограничение на структуру файлов — весьма дурацкое и особо не помогает, мой оппонент подумал, что я его троллю:) Так-то класс может быть в разных модулях, может быть вложенным и т.п., т.е. это не просто “иди по этому пути”, и хоть какой-то поиск придется рано или поздно использовать.
Весь спор пересказывать не буду, а перейду сразу к выводам.
Я понял, что есть 2 принципиально разных аспекта в языке: ограничения и предохранители. Предохранители — это обычно просто замечательно, они позволяют избегать тупых ошибок. Например, явная проверка на null в Kotlin или владение памятью в Rust — это предохранители. Ограничения — это противоположность выразительности, т.е. что-то, что мешает сделать программисту то, что он хочет. “Я ограничен технологиями своего времени”.
Соответственно, ограничение и предохранитель легко спутать друг с другом (или дебатировать, что хорошо и что плохо). И вот тут-то и зарыт корень наших разногласий: мой коллега искренне считал, что структура папок в Java — это предохранитель и это хорошо, а для меня это еще одно ограничение. В этом плане мне гораздо важнее разложить код по модулям, по ролям и/или по шаблону какой-нибудь трехбуквенной архитектуры. Обычно языку на это плевать, но в Java будет привет от com.company.name.package.folder.
Мне нравятся выразительные языки. На мой взгляд, хреново и непонятно можно написать на любом языке. Да, в более выразительном языке возможностей может быть больше, но человеческая тупость вообще бесконечна. В свою очередь, выразительный язык предоставляет больше возможностей написать код хорошо. А предохранители должны этому способствовать.
И разумеется, опыт в более выразительных языках влияет на то, как разработчик пишет код. Однако говорить о том, что язык формирует культуру разработки — очень странно. Это как сказать, что ножницы формируют культуру парикмахерской, или молоток формирует культуру стройки. Если у вас инструмент формирует процессы и/или подходы — у меня для вас плохие новости…
Комменты телеграма на сайте и похвала роботам
На выходных удалил комментарии от телеги с сайта (которые comments.app). API и возможности администрирования с 2020 года совсем не изменились: например, до сих пор нельзя посмотреть все комменты на сайте. Да и не пользовался ими никто — ровно один комментарий был не от меня. Старые комменты спарсил краулером, написанным ChatGPT.
Нашел как подключить комментарии из канала. Повспоминал свои боли с Jekyll. С точки зрения самой телеги оказалось довольно нетривиально понять, как сделать так, чтобы была ссылка вела сразу на дискуссию с комментариями, а не тупо на пост. В итоге тупо вставил ссылку в конце поста, на компе работает, но на планшете — нет (не открывает комментарии, только пост). ¯\_(ツ)_/¯
Увы, нельзя комментировать старые посты, потому что для них нет соответствующего поста в группе обсуждений. Ну и фиг с ними, все равно их никто не читает :) Паравозиком приехала ссылка на комменты с Хабра (тоже не знаю зачем, даже я уже Хабр практически перестал читать).
Осталось дело за малым — добавить везде ID поста в телеге. Это около 250 файлов, и там не просто подряд числа. Тут очень хорошо повкалывал ChatGPT, прям аж похвалить его захотелось: всего за 3-4 итерации генерировал рабочие решения на питоне, причем мне даже особо не нужно было читать код.
В итоге все посты из телеги сохранил через экспорт канала (с доступом через API нахвался уже), потом сопоставил их с постами с сайта по заголовку. Часть файлов обновил руками — там заголовки не совпадали или были проблемы из-за кавычек. На все-про-все ушло минут 10, и это классно.
Немного напортачил с переносом строк. Хотел поправить башем, но ChatGPT внезапно отупел и его предложения не работали. Оказалось, что кончились токены на 4 версию. Т.е. если предыдущие версии я в основном ругал, то от четвертой в основном положительные впечатления остались.
Еще я сделал тупую опечатку, которую нашел не сразу. В итоге больше всего потратил на обработку крайних случаев, тупую опечатку и приколы с экранированием. Иронично, что первый пост, к которому можно оставить коммент — “Самые сложные проблемы в разработке” (ладно, для душнил есть еще пост про открытие комментариев). Но в целом результатом я доволен, наконец-то почувствовал продуктивность с ChatGPT. Сам подход напомнил скачивание архива — тоже из говна и палок, “лишь бы работало”.

Профдеформация и C
C/C++ (именно в таком сочетании) я в базовом варианте изучил за пару летних месяцев перед универом. Тогда для меня он был просто заменой паскалю, и уровень задач был соответствующий — всякая мелочевка для развлечения и разнообразные числодробилки. Универ со своими лабами не сильно что изменил (я получил автомат по программированию в те времена, когда по всем предметам надо было сдавать экзамены и не было балльной системы); Python, JavaScript и даже Java мимо пробегали, но всякие тесты булевой функции на монотонность проще писались на C/C++.
На первых двух работах мне даже платили за то, что я писал на C++, но я тогда был джуном и это все сейчас кажется несерьезным (хотя уже будучи зрелым специалистом написал на C++ модуль ядра для BSD, это был прикольный, но мимолетный опыт). Тем более что потом переключился на Python, оттуда на Scala и понеслось…
В общем, получается, что всерьез, профессионально, на Си я и не писал никогда. А вот в последние несколько месяцев я как раз этим и занимался в рамках научного проекта. И (вполне закономерно) оказалось, что процесс не сильно-то отличается от других языков.
Все стандартные паттерны в наличии, например:
- разделение интерфейса (
.h) и реализации (.c) - своего рода полиморфизм можно построить на структурах и указателях на функции
- разделение
apiиimpementationв зависимостях (где включать заголовок — в.cили.h) - всякие билдеры, стратегии и фасады — это вообще легко
typedef— one love ❤️- даже шаблоны можно сделать, правда макросами
- и т.п.
При этом возникает понимание многих конструкций, которые казались “лишними”: extern, static, макросы (увы, некоторые прикольные штуки только ими и получается делать), префиксы для функций из разных модулей (даже в не очень большом проекте словил коллизию имен, неймспейсов не хватает), #ifdef DEBUG и отдельная сборка под valgrind или санитайзеры (потому что без отладочного -g особо и не отладишь утечки памяти, а еще valgrind не знает всех инструкций из -march=native и может даже врать про номера строк на -03). Более того, -Wall выдает все замечания по делу! Хотя inline с его приколами все еще невнятный какой-то :/
В более высокоуровневых языках обычно многие вещи делаются гораздо проще (но не во всех конечно *выразительно смотрит на java*), да и “мыслишь” после них более абстрактно. Часто себя ловил на мысли, что вот тут лямбду надо бы, а их особо и нет (только указатели на функции), а вот тут хотелось бы иметь возможность тип менять (вместо void *), а вот тут частичное применение функции прям зашло бы… Не хватает простых вещей типа Option, а указатели уже не хочешь использовать, потому что дешевле структуру передать.
Увы, инструменты разработки — не сильная сторона Си. Makefile еще можно потерпеть, autoconf сотоварищи — просто жесть, пакетный менеджер — мимо, VS Code опять выбесил по какой-то фигне, а добил меня миллион настроек clang-format, после которых я “обманываю” форматтер пустым комментарием, чтобы не совсем отвратно выдавал список аргументов функции. Впрочем, ничего нового (осторожно, по ссылке кринж). После космических технологий вроде IntelliJ Idea или Gradle — все очень грустно.
При этом язык все еще развивается. Например, весьма пригодились составные литералы:
return (some_struct_t){
.field1 = value1,
.field2 = .value2, // trailing comma FTW!
}
Сейчас есть стандарт c17, а еще грядет c23 — и там есть много прикольных штук, про многие из которых я могу сказать: да, такая фича пригодилась бы! Даже лямбды маячат на далеком горизонте, но добавить их — непростая задача.
В общем, писать что-то на низкоуровневом языке достаточно интересно (если это не Zig :)). Это полезное упражнение, чтобы понять, как много делают всякие хорошие инструменты и библиотеки, да и собственный прогресс оценить. “Вернуться к истокам” тоже прикольно. Когда писал пост, откопал в папке со своей универской фигней вот такую хрень, которая датируется 2010 годом:
#include <stdio.h>
class foo
{
friend bool operator < ( bool left, const foo& right );
friend int operator ^ ( int left, const foo& right );
};
bool operator < ( bool left, const foo& right ) { return left; }
int operator ^ ( int left, const foo& right ) { return left; }
int main()
{
int O_o = 0, _ = 0, baka = 0; foo o_O, neko;
bool XD = O_o >_< o_O;
int nya = baka ^_^ neko;
//printf("%d ",nya);
printf("%d ",XD);
return 0;
}
Очевидно, все вышеизложенное хорошо так субъективизировано связанными воспоминаниями из тех времен :) Но даже с учетом этого впечатления останутся положительными.
Что бы там не пророчили, кажется, что Си пока рано умирать.
