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

в 11:27, , рубрики: C, c++, clang-format, Git, java, javascript, linux, objective-c, protobuf, Программирование, Производство и разработка электроники, Разработка под Linux, Совершенный код

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

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

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

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

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

Для форматирования мы использовали clang-format-diff-6.0. На старте запустили команду
git diff -U0 --no-color | clang-format-diff-6.0 -i -p1, но с ней возникли проблемы:

  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 ${GIT_DIFF} --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, поэтому большинство редакторов кода, поддерживают автоматическую вставку такого перевода в конец файла. Без этого скрипт будет падать при коммите нового файла, но это не принесет никакого вреда.

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

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

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

Отладка

Если хотите посмотреть, какие изменения сделает скрипт на ваших текущих правках (не в индексе), используйте 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.

Автор: Promwad

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js