Главная » Хабрахабр » Форматирование исходного кода в Linux средствами ClangFormat: проблемы и решение

Форматирование исходного кода в Linux средствами ClangFormat: проблемы и решение

Это облегчает его понимание и поддержку. Согласитесь, приятно и полезно, когда в проекте исходный код выглядит красиво и единообразно. Покажем и расскажем, как реализовать форматирование исходного кода при помощи clang-format, git и sh.

Проблемы с форматированием и как их решить

Как сделать так, чтобы все участники их выполняли? В большинстве проектов существуют определенные правила оформления кода. На помощь приходят специальные программы — clang-format, astyle, uncrustify, — но у них есть свои недостатки.

Расскажем, как мы с этим справились, используя ClangFormat в рамках одного из проектов по разработке встроенного ПО для электроники, где С++ был основным языком. Главная проблема форматеров состоит в том, что они меняют файлы целиком, а не только изменённые строки. Наше решение может подойти не только программистам С++, но и тем, кто пишет код на C, Objective-C, JavaScript, Java, Protobuf. В команде работало несколько человек, поэтому для нас было важно обеспечить единый стиль кода.

0. Для форматирования мы использовали clang-format-diff-6. На старте запустили команду

0 -i -p1, но с ней возникли проблемы:
git diff -U0 --no-color | clang-format-diff-6.

  1. Программа определяла типы файлов только по расширению. Например, файлы с расширением ts, которые у нас имели формат xml, воспринимала как JavaScript и падала при форматировании. Потом, она зачем-то пыталась поправить pro-файлы проектов Qt, наверное, как Protobuf.
  2. Программу приходилось запускать вручную, перед добавлением файлов в индекс git. Легко было об этом забыть.

Решение

В результате получился следующий sh-скрипт, запускаемый как pre-commit — хук для git:

#!/bin/sh CLANG_FORMAT="clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*\.(cxx|cpp|hpp|h)$' "
GIT_DIFF="git diff -U0 --no-color "
GIT_APPLY="git apply -v -p0 - "
FORMATTER_DIFF=$(eval $ --staged | eval ${CLANG_FORMAT}) echo "\n------Format code hook is called-------" if [ -z "${FORMATTER_DIFF}" ]; then echo "Nothing to be formatted"
else echo "${FORMATTER_DIFF}" echo "${FORMATTER_DIFF}" | eval ${GIT_APPLY} --cached echo " ---Format of staged area completed. Begin format unstaged files---" eval ${GIT_DIFF} | eval ${CLANG_FORMAT} | eval ${GIT_APPLY}
fi echo "------Format code hook is completed----\n"
exit 0

Что делает скрипт:
GIT_DIFF=" git diff -U0 --no-color " — изменения в коде, которые подадут на вход clang-format-diff-6.0.

  • -U0: обычно git diff выводит так называемый «контекст»: несколько неизменёных строк кода вокруг тех, что были изменены. Но clang-format-diff-6.0 форматирует их тоже! Поэтому контекст в данном случае не нужен.

CLANG_FORMAT=" clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*\.(cxx|cpp|hpp|h)$' " — команда для форматирования diff, полученного через стандартный ввод.

  • clang-format-diff-6.0 — скрипт из пакета clang-format-6.0. Есть другие версии, но все тесты были только на этой.
  • -p1 взят из примеров в документации, обеспечивает совместимость с выводом git diff.
  • -style=Chromium — готовый пресет стиля форматирования кода. Другие возможные значения: LLVM, Google, Mozilla, WebKit.
  • -sort-includes — опция сортировки по алфавиту директив #include (не обязательна).
  • -iregex '.*\.(cxx|cpp|hpp|h)$' — регулярное выражение, фильтрующее имена файлов по расширениям. Тут перечислены только те расширения, которые надо форматировать. Это убережёт программу от падения и неожиданных глюков. Скорее всего список нужно будет дополнить в новых проектах. Кроме С++ можно форматировать C/Objective-C/JavaScript/Java/Protobuf. Хотя эти типы файлов мы не тестировали.

GIT_APPLY=" git apply -v -p0 — " — применение к коду патча, выданного предыдущей командой.

  • -p0: по умолчанию git apply пропускает первый компонент в пути к файлу, это несовместимо с форматом, который выдаёт clang-format-diff-6.0. Здесь отключено такое пропускание.

FORMATTER_DIFF=$(eval ${GIT_DIFF} --staged | eval ${CLANG_FORMAT}) — изменения форматера для индекса.

К сожалению, нет такого хука, который срабатывал бы перед добавлением файлов в индекс. echo "${FORMATTER_DIFF}" | eval ${GIT_APPLY} --cached форматирует исходный код в индексе (после git add). Поэтому форматирование разделено на две части: форматируется то, что в индексе и отдельно то, что не добавлено в индекс.

Форматирует вообще все текущие изменения в проекте (под контролем версий), а не только из предыдущего шага. eval ${GIT_DIFF} | eval ${CLANG_FORMAT} | eval ${GIT_APPLY} — форматирование кода не в индексе (запускается, только когда что-то было отформатировано в индексе). Но оно оказалось удобным, т.к. Это спорное, на первый взгляд, решение. Можно заменить "| eval ${GIT_APPLY}" опцией -i, которая заставит ${CLANG_FORMAT} менять файлы самостоятельно. рано или поздно другие изменения надо форматировать тоже.

Демонстрация работы

  1. Установить clang-format-6.0
  2. cd /tmp && mkdir temp_project && cd temp_project
  3. git init
  4. Добавить под контроль версий и закомитить любой файл C++ под именем wrong.cpp. Желательно >50 строк неформатированного кода.
  5. Сделать скрипт .git/hooks/pre-commit, показанный выше.
  6. Назначить скрипту права на запуск (для git): chmod +x .git/hooks/pre-commit.
  7. Запустить вручную скрипт .git/hooks/pre-commit, он должен запускаться с сообщением «Nothing to be formatted», без ошибок интерпретатора.
  8. Создать file.cpp с содержимым int main() { for (int i = 0; i < 100; ++i) { std::cout << " First case " << std::endl; std::cout << " Second case " << std::endl; std::cout << " Third case " << std::endl; } } одной строкой или с другим плохим форматированием. В конце — перевод строки!
  9. git add file.cpp && git commit -m " file.cpp " должны быть сообщения от скрипта типа «Патч file.cpp применен без ошибок».
  10. git log -p -1 должен показать добавление форматированного файла.
  11. Если file.cpp попал в коммит действительно форматированным, значит можно тестировать форматирование только в diff. Измените пару строк wrong.cpp так, чтобы форматер на них среагировал. Например, добавьте неадекватные отступы в коде вместе с другими изменениями. git commit -a -m " Format only diff " должен залить форматированные изменения, но не затронуть другие части файла.

Недостатки и проблемы

git diff --staged (который здесь ${GIT_DIFF} --staged) выдаёт diff только тех файлов, что были добавлены в индекс. А clang-format-diff-6.0 обращается к полным версиям файлов за пределами него. Поэтому, если изменить какой-то файл, сделать git add, а потом изменить тот же файл, то clang-format-diff-6.0 будет генерировать патч для форматирования кода (в индексе) на основе отличающегося файла. Таким образом, файл после git add и до коммита лучше не редактировать.

Вот пример такой ошибки:

  1. Добавить в file.cpp, " Second case " лишний std::endl. (std::cout << " Second case " << std::endl << std::endl;) и несколько табов лишнего отступа перед строкой.
  2. git add file.cpp
  3. Очистить строку (в этом же файле) с " First case " так, что бы на её месте остался(!) только перенос строки.
  4. git commit -m " Formatter error on commit ".

Скрипт должен сообщить " error: при поиске: ", т.е. git apply не нашёл контекст патча, выданного clang-format-diff-6.0. Если вы не поняли, в чём тут проблема, просто не меняйте файлы после git add их и до git commit. Если надо поменять, можете сделать коммит (без push) и потом git commit --amend с новыми изменениями.

Это старая особенность git, поэтому большинство редакторов кода, поддерживают автоматическую вставку такого перевода в конец файла. Самое серьёзное ограничение — необходимость иметь в конце каждого файла перевод строки. Без этого скрипт будет падать при коммите нового файла, но это не принесет никакого вреда.

0 форматирует код неадекватно. Очень редко clang-format-diff-6. Либо, окружить проблемный код комментариями, /* clang-format off */ и /* clang-format on */.
В этом случае можно добавить какие-нибудь бесполезные элементы в код, типа точки с запятой.

0 может выдавать неадекватный патч. Также clang-format-diff-6. Причина — внутри clang-format-diff. Это заканчивается тем, что git apply не принимает его, и код части коммита остается неотфоматированным. В этом случае можно посмотреть на патч форматирования с помощью команды git diff -U0 --no-color HEAD^ | clang-format-diff-6. Нет времени разбираться во всех ошибках программы. Самым простым решением будет добавление опции -i к предыдущей команде. 0 -p1 -v -sort-includes -style=Chromium -iregex '.*\.(cxx|cpp|hpp|h)$'. Если не помогло, можно попробовать форматирование для отдельных файлов целиком clang-format-6. В этом случае утилита не будет выдавать патч, а отформатирует код. Далее git add file.cpp и git commit --amend. 0 -i -sort-includes -style=Chromium file.cpp.

(Здесь его заменяет опция -style=Chromium).
Есть предположение, что чем ближе ваш конфиг .clang-format к одному из пресетов, тем меньше таких ошибок вы увидите.

Отладка

Если хотите посмотреть, какие изменения сделает скрипт на ваших текущих правках (не в индексе), используйте git diff -U0 --no-color | clang-format-diff-6.0 -p1 -v -sort-includes -style=Chromium -iregex '.*\.(cxx|cpp|hpp|h)$' Также можно проверить, как будет работать скрипт на последних коммитах, например, на тридцати: git filter-branch -f --tree-filter " ${PWD}/.git/hooks/pre-commit " --prune-empty HEAD~30..HEAD . Данная команда должна была форматировать предыдущие коммиты, но по факту меняет только их id. Поэтому стоит проводить такие эксперименты в отдельной копии проекта! После она станет непригодной для работы.

Заключение

Субъективно, от такого решения гораздо больше пользы чем вреда. Но надо тестировать поведение clang-format-diff разных версий на коде вашего проекта, с конфигом для вашего стиля кода.

Предлагайте в комментариях, как это сделать там. К сожалению, такой же git-hook для Windows мы не делали. А если нужна статья для быстрого старта с clang-format, советуем посмотреть описание ClangFormat.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Такая боль, такая боль, сервис на аутсорсе 1:0

В идеальном мире мы бы занимались только разработкой и развитием системы. Мы делаем самую лучшую в России и ближнем зарубежье систему обнаружения заимствований. Наш софт пока не работает без железа, пользователям нужно оказывать техническую поддержку, получать оплату от пользователей необходимо ...

Типовые ошибки пассажиров железных дорог и авиалиний

Мы вторая линия поддержки пассажиров. Привет! Я очень хочу рассказать, что может пойти не так, так как очень надеюсь, что это спасёт чьи-то нервы. Каждый день мы обрабатываем сотни ошибок в билетах. По крайней мере, мы постоянно сталкиваемся с проблемами ...