📜 ⬆️ ⬇️

Formatting Linux source code with ClangFormat: problems and solution



Agree, it is pleasant and useful when in the project the source code looks nice and consistent. This facilitates his understanding and support. We show and tell you how to implement the formatting of the source code using clang-format , git and sh .

Formatting problems and how to solve them


In most projects, there are certain rules for code design. How to make so that all participants performed them? Special programs come to the rescue - clang-format, astyle, uncrustify, but they have their drawbacks.

The main problem with formatters is that they change entire files, not just changed lines. Let us tell you how we coped with this using ClangFormat in one of the projects for developing embedded software for electronics, where C ++ was the main language. The team employed several people, so it was important for us to ensure a uniform style of code. Our solution can be suitable not only for C ++ programmers, but also for those who write code in C, Objective-C, JavaScript, Java, Protobuf.
')
For formatting, we used clang-format-diff-6.0 . At the start of the run command

git diff -U0 --no-color | clang-format-diff-6.0 -i -p1 , but problems arose with it:


  1. The program defined file types only by extension. For example, files with the ts extension, which we had in xml format, perceived as javascript and dropped during formatting. Then, for some reason, she tried to fix the pro-files of Qt projects, probably, like Protobuf.
  2. The program had to be run manually before adding files to the git index. It was easy to forget about it.

Decision


The result is the following sh script, run as a pre-commit hook for 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 

What the script does:
GIT_DIFF = "git diff -U0 --no-color" - changes in the code that will be input to clang-format-diff-6.0.


CLANG_FORMAT = "clang-format-diff-6.0 -p1 -v -sort-includes -style = Chromium -iregex '. * \. (Cxx | cpp | hpp | h) $'" - the command to format the diff received through the standard input


GIT_APPLY = "git apply -v -p0 -" —applies the patch code issued by the previous command to the code.


FORMATTER_DIFF = $ (eval $ {GIT_DIFF} --staged | eval $ {CLANG_FORMAT}) - changes the formatter for the index.

echo "$ {FORMATTER_DIFF}" | eval $ {GIT_APPLY} --cached formats the source code in the index (after git add ). Unfortunately, there is no such hook that would work before adding files to the index. Therefore, formatting is divided into two parts: what is in the index and what is not added to the index is formatted separately.

eval $ {GIT_DIFF} | eval $ {CLANG_FORMAT} | eval $ {GIT_APPLY} - code formatting is not in the index (it starts only when something has been formatted in the index). It formats all current changes in the project in general (under version control), and not just from the previous step. This is a controversial, at first glance, decision. But it turned out to be convenient, since Sooner or later, other changes need to be formatted too. You can replace "| eval $ {GIT_APPLY}" with the -i option, which will force $ {CLANG_FORMAT} to change files by itself.

Demonstration of work


  1. Install clang-format-6.0
  2. cd / tmp && mkdir temp_project && cd temp_project
  3. git init
  4. Add under version control and commit any C ++ file named wrong.cpp . Preferably> 50 lines of unformatted code.
  5. Make the .git / hooks / pre-commit script shown above.
  6. Assign launch permissions to the script (for git): chmod + x .git / hooks / pre-commit .
  7. To manually start the .git / hooks / pre-commit script, it should be run with the message “Nothing to be formatted” , without interpreter errors.
  8. Create file.cpp with the contents of 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; }} in one line or with another bad formatting. At the end - a line break!
  9. git add file.cpp && git commit -m "file.cpp" should be messages from a script like "Patch file.cpp applied without errors . "
  10. git log -p -1 should show adding a formatted file.
  11. If file.cpp hit the commit is really formatted, then you can test formatting only in diff. Modify a couple of lines of wrong.cpp so that the formatter responds to them. For example, add inadequate indentation in the code along with other changes. git commit -a -m "Format only diff" should fill in the formatted changes, but not affect other parts of the file.

Disadvantages and problems


git diff --staged (which here is $ {GIT_DIFF} --staged ) gives diff only those files that have been added to the index. And clang-format-diff-6.0 refers to full versions of files outside of it. Therefore, if you change a file, make git add , and then change the same file, clang-format-diff-6.0 will generate a patch for formatting the code (in the index) based on a different file. Thus, it is better not to edit the file after git add and before the commit.

Here is an example of such an error:

  1. Add an extra std :: endl to file.cpp , "Second case" . (std :: cout << "Second case" << std :: endl << std :: endl;) and several tabs of extra indentation before the line.
  2. git add file.cpp
  3. Clear the line (in the same file) with "First case" so that in its place there would be (!) Only the line break.
  4. git commit -m "Formatter error on commit" .

The script should report "error: when searching:" , i.e. git apply did not find the context of the patch issued by clang-format-diff-6.0 . If you do not understand what the problem is, just do not change the files after git add them and before git commit . If you need to change, you can make a commit (without push) and then git commit --amend with new changes.

The most serious limitation is the need to have a newline at the end of each file. This is an old git feature, so most code editors support the automatic insertion of such a translation at the end of the file. Without this, the script will fall when a new file is commited, but this will not do any harm.


Very rarely does clang-format-diff-6.0 format the code inadequately. In this case, you can add any useless elements to the code, such as a semicolon. Or, surround the problem code with comments, / * clang-format off * / and / * clang-format on * / .


Also, clang-format-diff-6.0 may issue an inadequate patch. This ends with git apply not accepting it, and the code of the commit part remains unformatted. The reason is inside clang-format-diff . No time to understand all the errors of the program. In this case, you can look at the formatting patch with the git diff -U0 command - no-color HEAD ^ | clang-format-diff-6.0 -p1 -v -sort-includes -style = Chromium -iregex '. * \. (cxx | cpp | hpp | h) $' . The easiest solution is to add the -i option to the previous command. In this case, the utility will not issue a patch, but will format the code. If it does not help, you can try formatting for individual files as a whole clang-format-6.0 -i -sort-includes -style = Chromium file.cpp . Next, git add file.cpp and git commit --amend .

There is an assumption that the closer your .clang-format config to one of the presets, the less such errors you will see. (This is replaced by the option -style = Chromium ).


Debugging


If you want to see what changes the script makes on your current edits (not in the index), use git diff -U0 --no-color | clang-format-diff-6.0 -p1 -v -sort-includes -style = Chromium -iregex '. * \. (cxx | cpp | hpp | h) $' You can also check how the script will work on recent commits, for example , at thirty: git filter-branch -f --tree-filter "$ {PWD} /. git / hooks / pre-commit" --prune-empty HEAD ~ 30..HEAD . This command should have formatted previous commits, but in fact only changes their id. Therefore it is worth carrying out such experiments in a separate copy of the project! After it becomes unsuitable for work.

Conclusion


Subjectively, from such a decision is much more benefit than harm. But it is necessary to test the behavior of clang-format-diff of different versions on the code of your project, with a config for your code style.

Unfortunately, we didn’t do the same git-hook for Windows. Suggest in the comments how to do it there. And if you need an article for a quick start with clang-format , we advise you to look at the description of ClangFormat .

Source: https://habr.com/ru/post/433832/


All Articles