pre-commit hooks for the ROS 2 core code formatters and linters.
Warning
This repository is STILL EXPERIMENTAL, it is not yet in use on any ROS core packages
These hooks are the ament_lint linters with their ROS 2 configurations, run by pre-commit instead of CMake.
One hook is provided per ament_cmake_<tool> package, so a package.xml test dependency list translates one-to-one onto hook ids.
Consuming repositories reference this repo directly via .pre-commit-config.yaml.
No config files need to be copied or kept in sync.
See docs/DESIGN.md for the reasoning and DEVELOPING.md for development workflows.
Install pre-commit. Our recommended approach is with uv.
uv tool install --with pre-commit-uv pre-commitSet up pre-commit hooks in the repository:
pre-commit installros-uncrustify also needs uncrustify on PATH.
See its hook reference entry.
Add the following to your repository's .pre-commit-config.yaml.
This block is equivalent to ament_lint_common.
---
repos:
- repo: https://github.com/ros-tooling/ros_code_standard
rev: v0.1.1
hooks:
- id: ros-copyright
- id: ros-cppcheck
- id: ros-cpplint
- id: ros-flake8
- id: ros-lint-cmake
- id: ros-pep257
- id: ros-uncrustify
- id: ros-xmllintOptional linters that are not part of ament_lint_common:
- id: ros-clang-format
- id: ros-clang-tidy
args: [--build-dir, build]
- id: ros-mypy
- id: ros-pycodestyle
- id: ros-pyflakesRemove the linter test dependencies from package.xml:
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>Remove the linter block from CMakeLists.txt:
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
ament_lint_auto_find_test_dependencies()
endif()If the package listed individual ament_cmake_<tool> dependencies instead of ament_lint_common, add the matching ros-<tool> hook for each one.
Packages that call the linters explicitly carry per-linter options that map onto hook fields:
| CMake | pre-commit |
|---|---|
ament_cpplint(EXCLUDE path ...), AMENT_LINT_AUTO_FILE_EXCLUDE, AMENT_IGNORE |
exclude: ^path on that hook |
ament_cppcheck(LANGUAGE c++), ament_uncrustify(LANGUAGE c++) |
args: [--language, c++] |
ament_cpplint(FILTERS -a -b), ament_lint_cmake(FILTERS ...) |
args: [--filters=-a,-b] |
ament_cpplint(MAX_LINE_LENGTH 120), ament_flake8(MAX_LINE_LENGTH 120) |
args: [--linelength, '120'] |
ament_cppcheck(INCLUDE_DIRS a b), ament_cppcheck(LIBRARIES a b) |
args: [--include-dirs, 'a,b', --libraries, 'a,b'] |
AMENT_LINT_AUTO_EXCLUDE ament_cmake_<tool> |
leave ros-<tool> out of the hook list |
For example, rcpputils excludes a vendored header from four linters and forces C++ for two:
- id: ros-copyright
exclude: ^include/rcpputils/tl_expected/
- id: ros-cppcheck
args: [--language, c++]
exclude: ^include/rcpputils/tl_expected/
- id: ros-cpplint
exclude: ^include/rcpputils/tl_expected/
- id: ros-lint-cmake
- id: ros-uncrustify
args: [--language, C++]
exclude: ^include/rcpputils/tl_expected/
- id: ros-xmllintApply your newly configured hooks to all existing files:
pre-commit run --all-filesStage the reformatted files, then run again to surface any failures that require manual correction.
Note
After a large reformatting pass, add the commit hash to .git-blame-ignore-revs so git blame points back to the original authors rather than the reformatting commit.
Add the following GitHub Actions workflow to run pre-commit on every push and pull request:
---
name: Lint
on:
push:
branches: [main]
pull_request:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
# Only needed for ros-uncrustify.
- run: sudo apt-get install -y --no-install-recommends uncrustify
- uses: pre-commit/action@v3.0.1Hook ids mirror the ament_cmake_<tool> package names.
The first eight are the ament_lint_common set.
| Hook | ament package | Tool |
|---|---|---|
ros-copyright |
ament_cmake_copyright |
ament_copyright |
ros-cppcheck |
ament_cmake_cppcheck |
cppcheck |
ros-cpplint |
ament_cmake_cpplint |
cpplint |
ros-flake8 |
ament_cmake_flake8 |
flake8 |
ros-lint-cmake |
ament_cmake_lint_cmake |
cmakelint |
ros-pep257 |
ament_cmake_pep257 |
pydocstyle |
ros-uncrustify |
ament_cmake_uncrustify |
uncrustify |
ros-xmllint |
ament_cmake_xmllint |
lxml |
ros-clang-format |
ament_cmake_clang_format |
clang-format |
ros-clang-tidy |
ament_cmake_clang_tidy |
clang-tidy |
ros-mypy |
ament_cmake_mypy |
mypy |
ros-pycodestyle |
ament_cmake_pycodestyle |
pycodestyle |
ros-pyflakes |
ament_cmake_pyflakes |
pyflakes |
ament_cmake_pclint has no hook.
PC-lint Plus is proprietary and is not part of any ROS 2 default.
Hook arguments that take a list are comma separated, and a value that starts with - must use the --option=value form so it is not read as another option.
Note
ros-uncrustify and ros-clang-format are both in-place C++ formatters and their ament configurations disagree on short function bodies and constructor initializer lists.
Enable one or the other, not both.
ament_lint_common uses uncrustify.
Runs ament_copyright over staged C, C++, CMake, and Python files, and over the repository's LICENSE and CONTRIBUTING.md when they exist.
Checks that each file carries a copyright notice and a recognized license header, and that the two repository-level files match one of the known license texts.
A setup.py beside a package.xml is skipped, as ament_copyright does when it crawls a package.
Optional:
--add-missing COPYRIGHT_NAME LICENSE-- Insert a notice and header into files that have none, using the given copyright holder and license name (for exampleapache2), then check. The hook fails when anything was inserted so the files get re-staged.--verbose-- Report every file checked, not only the ones with errors
- id: ros-copyright
args: [--add-missing, 'Polymath Robotics, Inc.', apache2]Run ament_copyright --list-licenses in the hook environment for the license names.
Runs cppcheck with the ament_cppcheck flag set (-f --inline-suppr -q -rp --suppress=internalAstError --suppress=unknownMacro) over the staged C and C++ files, one job per core, and fails on any finding.
ament_cppcheck skips every cppcheck 2.x release as too slow for a whole package, and Ubuntu 22.04 and 24.04 both ship 2.x, so on current ROS 2 distributions ament_lint_common never actually runs cppcheck.
This hook always runs, so expect findings that colcon test never showed you.
Optional:
--language {c,c++}-- Force cppcheck to treat every file as this language--libraries NAMES-- Comma separated cppcheck library configurations to load in addition to the standard C and C++ ones--include-dirs DIRS-- Comma separated include directories for the files being checked
- id: ros-cppcheck
args: [--language, c++, --libraries, posix, --include-dirs, 'include,src/private']Runs ament_cpplint, the ROS 2 linter itself, over the staged C and C++ files.
It is not PyPI cpplint.
ament vendors a modified fork of cpplint 1.5.5, and the checks cpplint 2.0 added do not match ROS 2 style: a package that passes colcon test reports build/c++17, whitespace/newline, and a wider build/include_what_you_use under the PyPI release.
Because this hook calls ament_cpplint, its findings, filter set, line length of 100, --root grouping by the nearest include, src, or test ancestor, and the ROS header guard convention (include/my_pkg/foo.hpp guards on MY_PKG__FOO_HPP_) are identical to what colcon test reports.
Optional:
--filters=FILTER,FILTER-- Extra cpplint category filters, appended to ament's defaults--linelength N-- Maximum line length. ament uses 100--root PATH-- Use this cpplint root for every file instead of the one ament computes from the path
- id: ros-cpplint
args: [--filters=-build/include_order, --linelength, '120']Vendored or generated C++ that a package excluded from its CMake lint test needs an exclude: pattern here, since the hook only sees a file list:
- id: ros-cpplint
exclude: ^include/rcpputils/tl_expected/Runs flake8 on Python files with the ament_flake8 configuration and its seven plugins: flake8-blind-except, flake8-builtins, flake8-class-newline, flake8-comprehensions, flake8-deprecated, flake8-quotes, and flake8-import-order (Google import order style, max line length 99).
The configuration is passed with --config, which flake8 treats as authoritative, so a setup.cfg, tox.ini, or .flake8 in your repository is not read and cannot weaken the ROS 2 style.
Optional:
--linelength N-- Maximum line length, overriding the 99 in the ament configuration
- id: ros-flake8
args: [--linelength, '120']Runs ament_lint_cmake over CMakeLists.txt, *.cmake, and *.cmake.in files.
This is ament_lint_cmake itself, not PyPI cmakelint with ament's flags.
The ament package vendors a modified fork of cmakelint, so running the real thing means the findings match colcon test exactly, and the 140-character line length, the ROS 2 parenthesis and string rules, and the handling of *.cmake.in templates all come from ament.
Per-file # lint_cmake: <filters> pragmas work as they do under colcon test.
Note
Like ament, this hook reads .cmakelintrc from the working directory, from $XDG_CONFIG_DIR, and from your home directory when present, so a personal rc file can change the result.
That is kept for parity with colcon test.
Optional:
--filters=FILTERS-- Comma-separated cmakelint category filters, each prefixed with+or-, passed straight toament_lint_cmake. Same meaning as theFILTERSargument of the CMake macro
- id: ros-lint-cmake
args: [--filters=-readability/mixedcase,-convention/filename]Runs pydocstyle on Python files with the ament_pep257 ignore list, which ament calls the ament convention: D100 to D107, D203, D212, and D404 are ignored.
That leaves D211 and D213 active, so a class docstring starts on the line after the class statement and a multi-line docstring summary starts on the line after the opening quotes.
Unlike bare pydocstyle, files whose names begin with test_ are checked.
Optional:
--ignore CODES-- Comma separated codes for pydocstyle not to check, replacing the ament list (mutually exclusive with--selectand--convention)--select CODES-- Comma separated codes for pydocstyle to check, replacing the ament list (mutually exclusive with--ignoreand--convention)--convention NAME-- A preset list:ament(default),google,numpy, orpep257(mutually exclusive with--ignoreand--select)--add-ignore CODES-- Comma separated codes to remove from the selected list--add-select CODES-- Comma separated codes to add to the selected list
- id: ros-pep257
args: [--add-ignore, 'D213,D205']Runs uncrustify with the ament_code_style configuration, the same one ament_uncrustify ships.
uncrustify --version selects the config: 0.78.1 and newer get ament_code_style_0_78.cfg, anything older gets ament_code_style_0_72.cfg.
Ubuntu 24.04 ships 0.78.1 and Ubuntu 22.04 ships 0.72.0, so both are supported.
Files are split into a -l C group (.c, .cc, .h, .hh) and a -l CPP group (.cpp, .cxx, .hpp, .hxx), matching ament_uncrustify, and each group is checked separately.
A C++ header named .h is therefore parsed as C, as in ament.
--language C++ is the escape hatch.
Files that need reformatting are rewritten in place and the hook fails so you re-stage them, with the unified diff of every change in the output.
This differs from ament_uncrustify, which prints the diff and leaves the files alone.
Nothing else is written to your repository.
Note
Requires uncrustify on PATH.
There is no PyPI package and no upstream Linux binary, so it cannot be installed into the hook virtualenv.
Ubuntu and Debian: sudo apt install uncrustify.
macOS: brew install uncrustify.
ROS 2 builds it through uncrustify_vendor, so sourcing a ROS 2 installation also puts a suitable version on PATH.
Optional:
--linelength N-- Override the config'scode_widthof 100--language {C,C++,CPP}-- Force uncrustify's-linstead of choosing it per file extension
- id: ros-uncrustify
args: [--language, C++]Checks XML files for well-formedness and validates them against the schemas they declare, using lxml rather than the libxml2 xmllint binary, so nothing needs to be installed outside the hook virtualenv.
Schemas come from an xml-model processing instruction or from xsi:noNamespaceSchemaLocation on the root element.
XML Schema and RelaxNG are both supported, and a relative reference is resolved next to the document, as ament_xmllint does.
The ROS package_format2.xsd and package_format3.xsd schemas are bundled with the hook, so validating a package.xml needs no network access.
Schematron references are recognized and skipped, which is the one thing ament_xmllint does that this hook does not.
No arguments.
Runs clang-format with the ament .clang-format style.
Reformats the staged C and C++ files in place and then fails, so you re-stage and recommit.
ament_clang_format is not part of ament_lint_common.
Its style disagrees with the ament uncrustify configuration on short function bodies and on constructor initializer lists, so enable this hook or ros-uncrustify, not both.
No arguments.
Runs clang-tidy over the staged C and C++ sources, using the compilation databases of a colcon build directory.
Translated from ament_clang_tidy.
Note
Experimental, and the only hook here that cannot work on a bare checkout. clang-tidy needs the compiler command line for every file it analyses, so the hook has to be pointed at a build tree:
colcon build --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ONcolcon writes one build/<package>/compile_commands.json per package.
Without --build-dir, or with a build directory that holds no database, the hook fails with that command rather than pretending to check anything.
The hook searches --build-dir recursively for compile_commands.json files, skipping . and _ directories, and runs one clang-tidy -p <database dir> per database with the staged files that database lists.
A path that names a file is treated as a compilation database itself.
Staged files that no database lists are reported as skipped, not as failures.
Headers are normally in this group: they have no entry of their own and are analysed through the sources that include them, which needs --header-filter.
A source that shows up as skipped has not been built yet.
Findings fail the hook.
clang-tidy exits 0 for warnings, so, like ament_clang_tidy when it builds its report, the hook fails on any reported diagnostic.
Required:
--build-dir PATH-- A colcon build directory to search forcompile_commands.json, or one such file
Optional:
--config PATH-- A.clang-tidyfile, passed as--config-file. Without it, clang-tidy looks for.clang-tidyup the source tree, asament_clang_tidydoes--header-filter REGEX-- Report diagnostics from the non-system headers matching this regex--fix-- Apply the suggested fixes with--fix-errors, then fail so you re-stage them--quiet-- Suppress the counts of ignored warnings--system-headers-- Report diagnostics from system headers too--explain-config-- Explain which configuration file enables each check--jobs N-- Number of compilation databases to analyse in parallel (default: 1)--extra-arg=ARG-- Append a compiler argument to every command line. Repeatable
- id: ros-clang-tidy
args: [--build-dir, build, --header-filter, 'include/my_package/.*']
exclude: ^test/Note
The bundled clang-tidy finds its own builtin headers, but it picks the newest GCC installation on the machine to locate the C++ standard library.
If that installation has no headers, every #include <cstddef> is reported as file not found and the hook fails for reasons unrelated to lint.
Point it at the right toolchain with --extra-arg=--gcc-install-dir=/usr/lib/gcc/x86_64-linux-gnu/11.
Note
ament_clang_tidy excludes gtest sources and everything under a package's test/ directory.
This hook lints what you staged, so use exclude: for the same effect.
Runs mypy on Python files with the ament_mypy configuration, which sets ignore_missing_imports = True.
The cache is directed at the null device, so no .mypy_cache is written into your repository.
Optional:
--strict-- Useament_mypy_strict.toml(strict = true,pretty = true) instead ofament_mypy.ini
- id: ros-mypy
args: [--strict]Note
mypy type-checks the imports of the files it is given, so results depend on what is installed in the hook virtualenv rather than in your workspace.
ignore_missing_imports = True keeps unresolved ROS message and client library imports from failing the hook.
Note
The hook passes --explicit-package-bases, so module names are derived from the repository root.
Without it, a workspace with more than one Python package fails immediately with Duplicate module named "setup", because pre-commit hands mypy every staged file at once instead of one package at a time.
Runs pycodestyle on Python files with the ament_pycodestyle configuration: max line length 99 and an empty ignore list.
Clearing the ignore list means E121, E123, E126, E226, E24, E704, W503, and W504 are all reported, which pycodestyle suppresses by default.
The configuration is passed with --config, so a [pycodestyle] section in your setup.cfg or tox.ini is not read.
Optional:
--linelength N-- Maximum line length, overriding the 99 in the ament configuration
- id: ros-pycodestyle
args: [--linelength, '120']Note
ros-pycodestyle is stricter than ros-flake8 on purpose, and it is not part of ament_lint_common.
ament_flake8.ini uses extend-ignore, so flake8 keeps its own default ignore list, while ament_pycodestyle.ini clears it.
A line break before a binary operator (W503) or x*y without spaces (E226) passes ros-flake8 and fails ros-pycodestyle.
Runs pyflakes on Python files, with no configuration, matching ament_pyflakes.
It reports unused imports, undefined names, unused local variables, and similar correctness problems, and says nothing about style.
No arguments.
- Files, not directories.
pre-commit hands each hook the staged files that match its type, so
AMENT_IGNOREmarkers are not honored. Useexclude:patterns instead. - Formatters fix.
ros-uncrustify,ros-clang-format, andros-copyright --add-missingrewrite files and fail so you re-stage them. ament only reports. - No xUnit output. The exit code is the result.
- ament's own tools where it matters.
ros-copyright,ros-cpplint, andros-lint-cmakerun theament_lint0.21.2 packages themselves, so their findings matchcolcon testexactly. The Python linters and cppcheck are current PyPI releases with ament's configuration; cppcheck is 2.17 where ament refuses to run 2.x at all.