Skip to content

Integrate clang-tidy into CI#112785

Merged
Repiteo merged 1 commit into
godotengine:masterfrom
Ivorforce:clang-tidy-ci
Feb 20, 2026
Merged

Integrate clang-tidy into CI#112785
Repiteo merged 1 commit into
godotengine:masterfrom
Ivorforce:clang-tidy-ci

Conversation

@Ivorforce

@Ivorforce Ivorforce commented Nov 14, 2025

Copy link
Copy Markdown
Member

Clang-tidy is the bigger, more powerful sibling of clang-format. The main difference is that clang-format parses files in isolation, and focuses on linting features, while clang-tidy trudges into the first steps of compilation, and can use more advanced information.

We already use clang-format. This PR introduces clang-tidy (in minimal form).

Motivation

I think we want (need!) clang-tidy for its many great codebase-quality related features, such as:

  • Remove unused includes
  • Force use of specific features (e.g. nullptr over NULL).
  • Flag bug-prone syntax or implementations
  • Enforce certain naming conventions
  • ... and many, many more

clang-tidy Initial State

Building on Repiteo's initial implementation, I focused on getting the "simplest" version of clang-tidy integrated that I could.

However, I needed to nerf it severely for it to run:

  • All features disabled (except header modernization).
    • Notably, it will still flag some important things, such as unused functions or missing includes
  • Whitelist directories to run on. Platform-specific directories fail because of missing header files.
  • Unknown warning diagnostics ignored (via https://stackoverflow.com/a/47043282/503822)

With everything nerfed, and problems fixed, the following command succeeded (after about 15m of runtime):

pre-commit run --all-files --hook-stage manual clang-tidy

I also removed cppcoreguidelines-pro-type-member-init for good, because it just generated false positives (as far as i can judge - it suggested using Class some_class{} instead of Class some_class for using the default initializer.

CI implementation

clang-tidy makes use of compile_commands.json to run properly. That means users will have to have it up-to-date for pre-commit to run as expected.
This is probably not acceptable in the standard workflow, so I kept the clang-tidy pre-commit hook as manual.

To integrate it, I add compiledb=yes to the build command of Linux builders, and let the fastest linux builder (Minimal) take the burden of running it. It should not make a difference on the actual runtime of CI.

Changes made

All necessary because of clang-tidy failures:

  • static free functions changed to inline. static forces compile units to keep their own version, leading to unnecessary engine bloat (and an "unused function" warning)
  • Incorrect #include <type_traits> changed to #include <utility>
  • C-header #include <stdint.h> changed to C++ header #include <cstdint>. In theory, this should reduce the number of ambiguous/error-prone free functions (but in practice, it doesn't make a difference in stdlib implementations)
Comment thread .github/workflows/linux_builds.yml Outdated
@akien-mga

Copy link
Copy Markdown
Member

With everything nerfed, and problems fixed, the following command succeeded (after about 15m of runtime):

Adding 15 min of build time to CI for every single workflow might end up costing a lot of time. And it sounds like it's a minimum and it would grow once we enable more checks and can integrate more files in the testing.

Based on the name clang-tidy is meant to tidy up mistakes, so maybe it would be fine to have it run on a cron job once in a while and we fix those issues manually, instead of making it a hard requirement for every single commit to pass a heavy series of checks.

At least judging by the minimal changes in this PR after months of not having run clang-tidy, I don't think paying 15 min per workflow to catch this kind of rare issues would be a good tradeoff.

@Ivorforce

Ivorforce commented Nov 14, 2025

Copy link
Copy Markdown
Member Author

The 15 minute runtime is only needed for running it against the entire codebase. PRs will only need to check changed files, which is usually completed in a few seconds.

Besides, this is only the minimum of functionality we might want, kept simple to be mergable. We'll probably want to integrate more of clang-tidy after.

@Ivorforce Ivorforce force-pushed the clang-tidy-ci branch 3 times, most recently from 46b1515 to 99745ae Compare November 14, 2025 22:39
@Ivorforce

Ivorforce commented Nov 14, 2025

Copy link
Copy Markdown
Member Author

Alright, it should be ready. clang-tidy definitely does run on the latest CI run now.

That being said, I have discovered I was somewhat wrong. While running clang-tidy on header files takes less than a second, running on a .cpp file can apparently take significantly longer. I can only assume it's because it's doing additional checks across the include chain.
Checking ustring.cpp for me locally took 1:30. Since the --all-files check takes 15m, that means any particular PR will probably have to wait 1:30 to 2m longer with this addition (or more, if CI is slower).

I might be OK with moving this to a cron job instead. But we should be aware that, if we decide to make heavy use of clang-tidy, we'll probably be tidying up after merges regularly.
Let me know what you think.

@Repiteo

Repiteo commented Nov 14, 2025

Copy link
Copy Markdown
Contributor

That being said, I have discovered I was somewhat wrong. While running clang-tidy on header files takes less than a second, running on a .cpp file can apparently take significantly longer. I can only assume it's because it's doing additional checks across the include chain.

That's indeed what's happening. iirc, clang-tidy has a way of specifying only specific changed sections to be scanned, but actually parsing that out and passing it requires more dedicated setup. I can't say for certain, as the testing branch I had for that was lost on my old hard-drive; will see if I have the time to re-create it over the weekend

@akien-mga akien-mga self-requested a review February 2, 2026 18:44
@akien-mga

This comment was marked as outdated.

@akien-mga

akien-mga commented Feb 2, 2026

Copy link
Copy Markdown
Member

Running with pre-commit run --hook-stage manual clang-tidy --all, I'm getting these error repeated a bunch of times:

/home/akien/Godot/.clang-tidy:8:1: error: duplicated mapping key 'CheckOptions'
CheckOptions:
^~~~~~~~~~~~
Error parsing /home/akien/Godot/.clang-tidy: Invalid argument

I fixed them with:

diff --git a/.clang-tidy b/.clang-tidy
index 7e43549f9c..6fb4741919 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -12,7 +12,7 @@ ImplementationFileExtensions: [c, cc, cpp, cxx, m, mm, java]
 HeaderFilterRegex: (core|doc|drivers|editor|main|modules|platform|scene|servers|tests)/
 FormatStyle: file
 CheckOptions:
-  modernize-deprecated-headers.CheckHeaderFile: true
-  modernize-use-bool-literals.IgnoreMacros: false
-  modernize-use-default-member-init.IgnoreMacros: false
-  modernize-use-default-member-init.UseAssignment: true
+  - modernize-deprecated-headers.CheckHeaderFile: true
+  - modernize-use-bool-literals.IgnoreMacros: false
+  - modernize-use-default-member-init.IgnoreMacros: false
+  - modernize-use-default-member-init.UseAssignment: true

It's weird though as the syntax used before is the one shown in the --help output.

After these it seems to run, though it's super slow. It's been running over 40 min on a single core and not done yet.
I see we're defining require_serial: true, removing it making it use all available cores. I guess there are issues with running multiple threads?

Edit: I tried to remove require_serial: true and it seemed to work ok for me, succeeding in 18 min.

Comment thread .github/workflows/linux_builds.yml Outdated
platform: linuxbsd
target: ${{ matrix.target }}

- name: Style checks via pre-commit
if: matrix.clang-tidy
uses: pre-commit/action@v3.0.1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can keep it general so we potentially get patch releases automatically.

Suggested change
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@v3

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried this but it didn't work.

Comment thread .pre-commit-config.yaml Outdated
# TODO Platform-specific subfolders currently fail, we should try to include them
files: ^(core|main|scene)/.*\.(c|h|cpp|hpp|cc|hh|cxx|hxx|m|mm|java)$
# No unknown warning suppression used for easier compatibility with gcc
args: [--fix, --quiet, --use-color, -p=compile_commands.json, -extra-arg=-Wno-unknown-warning-option]
types_or: [text]
additional_dependencies: [clang-tidy==20.1.0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could bump the version to 21.1.8 which is the current latest.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latest my pre-commit can find is 21.1.6, which we already use (after rebase).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specifically: pre-commit pulls from the pip upload of clang-tidy, which is indeed 21.1.6 at time of writing

@Ivorforce Ivorforce force-pushed the clang-tidy-ci branch 4 times, most recently from a2eb271 to bf0407b Compare February 3, 2026 14:50
@Ivorforce

Ivorforce commented Feb 3, 2026

Copy link
Copy Markdown
Member Author

I rebased the branch onto latest master and tested the build again (one new issue came up which I fixed).

@akien-mga I don't get the "CheckOptions" issue. It's especially confusing because pre-commit enforces a specific clang-tidy version, so we should definitely see the same behavior. Adding the - to the keys makes CLion complain (list instead of dict entry), so I'm not sure it's the right solution.

Regarding require-serial, clang-tidy succeeds in 12 minutes without it and 1:30 minutes with. That's a serious difference. I don't see why we require it to run in serial so I'll disable it.

@akien-mga

akien-mga commented Feb 3, 2026

Copy link
Copy Markdown
Member

It's very weird that it's so much slower for me. What OS are you on and which compiler and compile options are you using (in case this impacts what clang-tidy makes of the compiledb)?

I'm on Linux with GCC, on an AMD Ryzen 7 8845HS with 64 GiB RAM, which shouldn't be too shabby.
Compiling with scons dev_build=yes dev_mode=yes linker=mold scu_build=all cache_path=/home/akien/.scons_cache/godot/ fast_unsafe=yes compiledb=yes.

@akien-mga

Copy link
Copy Markdown
Member

Running with the latest version of this PR, with no local modifications, I get:


/home/akien/Godot/godot/core/extension/gdextension_interface.cpp:80:24: error: ordered comparison of function pointers ('GDExtensionCallableCustomCall' (aka 'void (*)(void *, const void *const *, long, void *, GDExtensionCallError *)') and 'GDExtensionCallableCustomCall') [clang-diagnostic-ordered-compare-function-pointers]
   80 |                         return a->call_func < b->call_func;
      |                                ~~~~~~~~~~~~ ^ ~~~~~~~~~~~~
3 warnings generated.
5 warnings generated.
16 warnings generated.
3 warnings generated.
3 warnings generated.
3 warnings generated.
3 warnings generated.
3 warnings generated.
3 warnings and 1 error generated.
Error while processing /home/akien/Godot/godot/core/extension/gdextension_interface.cpp.
@Ivorforce

Ivorforce commented Feb 3, 2026

Copy link
Copy Markdown
Member Author

It's very weird that it's so much slower for me. What OS are you on and which compiler and compile options are you using (in case this impacts what clang-tidy makes of the compiledb)?

I'm using macOS Tahoe (26), on an M1 Max with builtin apple clang.
I was using ❯ scons vulkan_sdk_path=/Users/lukas/dev/libs/MoltenVK tests=yes warnings=extra debug_symbols=yes compiledb=yes compile_commands.json for the build command.

Running with the latest version of this PR, with no local modifications, I get:

I don't get this one either. I don't know much about clang-tidy but going by the name, I'd expect it to use clang instead of gcc?
Perhaps here it's relevant that I'm using apple clang and you'd be using llvm clang (or just the version is relevant). The error seems not to be clang-tidy specific, rather internal to clang and usually issued as a warning.

@Repiteo

Repiteo commented Feb 3, 2026

Copy link
Copy Markdown
Contributor

Note that #112850 makes it possible for scons to generate only a compilation database via compiledb_gen_only=yes. I don't believe that alone makes it fast enough to incorporate into the static checks, but it might be worth testing how fast GHA handles it

@akien-mga

akien-mga commented Feb 3, 2026

Copy link
Copy Markdown
Member

I don't get this one either. I don't know much about clang-tidy but going by the name, I'd expect it to use clang instead of gcc?
Perhaps here it's relevant that I'm using apple clang and you'd be using llvm clang (or just the version is relevant). The error seems not to be clang-tidy specific, rather internal to clang and usually issued as a warning.

I just tested now with a compiledb generated using clang (use_llvm=yes added to my usual command) and it's much faster, 1m37s.

So it seems to be choking on GCC specific stuff maybe that slows it down? Maybe the errors I ran into also come from GCC specific flags? I'm not sure how clang-tidy uses the compiledb to set its own parameters. I remember playing with include-what-you-use years ago and it basically acted like a compiler, but clang-tidy doesn't seem to work exactly like that.

Here's an example of flags used for main.cpp with clang:

clang++ -o bin/obj/main/main.linuxbsd.editor.dev.x86_64.llvm.o -c -std=gnu++17 -fno-exceptions -Wctor-dtor-privacy -Wnon-virtual-dtor -ffp-contract=off -pipe -msse4.2 -mpopcnt -fno-color-diagnostics -O0 -gdwarf-4 -g3 -Wall -Wextra -Wwrite-strings -Wno-unused-parameter -Wshadow-field-in-constructor -Wshadow-uncaptured-local -Wno-ordered-compare-function-pointers -Wenum-conversion -Wimplicit-fallthrough -Werror -DTOOLS_ENABLED -DDEBUG_ENABLED -DDEV_ENABLED -DENGINE_UPDATE_CHECK_ENABLED -DNO_EDITOR_SPLASH -DSTRICT_CHECKS -DSCU_BUILD_ENABLED -DSOWRAP_ENABLED -DTOUCH_ENABLED -DFONTCONFIG_ENABLED -DALSA_ENABLED -DALSAMIDI_ENABLED -DPULSEAUDIO_ENABLED -D_REENTRANT -DDBUS_ENABLED -DSPEECHD_ENABLED -DXKB_ENABLED -DUDEV_ENABLED -DSDL_ENABLED -DLINUXBSD_ENABLED -DUNIX_ENABLED -D_FILE_OFFSET_BITS=64 -DX11_ENABLED -DLIBDECOR_ENABLED -DWAYLAND_ENABLED -DACCESSKIT_DYNAMIC -DACCESSKIT_ENABLED -DVULKAN_ENABLED -DRD_ENABLED -DGLES3_ENABLED -DCRASH_HANDLER_ENABLED -D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES -DMINIZIP_ENABLED -DBROTLI_ENABLED -DOVERRIDE_ENABLED -DOVERRIDE_PATH_ENABLED -DTHREADS_ENABLED -DCLIPPER2_ENABLED -DZLIB_DEBUG -DZSTD_STATIC_LINKING_ONLY -DUSE_VOLK -DVK_USE_PLATFORM_XLIB_KHR -DVK_USE_PLATFORM_WAYLAND_KHR -DGLAD_ENABLED -DEGL_ENABLED -DTESTS_ENABLED -DTESTS_ENABLED -Ithirdparty/freetype/include -Ithirdparty/libpng -Ithirdparty/glad -Ithirdparty/volk -Ithirdparty/vulkan -Ithirdparty/vulkan/include -Ithirdparty/accesskit/include -Ithirdparty/zstd -Ithirdparty/zlib -Ithirdparty/clipper2/include -Ithirdparty/brotli/include -Ithirdparty/linuxbsd_headers/libdecor-0 -Ithirdparty/linuxbsd_headers/wayland -Ithirdparty/linuxbsd_headers -Iplatform/linuxbsd -I. main/main.cpp

Here's the diff between our GCC and Clang build flags (in this config):

--- gcc 2026-02-03 20:46:40.326849283 +0100
+++ clang       2026-02-03 20:46:44.208589219 +0100
@@ -2,13 +2,11 @@
 -fno-exceptions
 -Wctor-dtor-privacy
 -Wnon-virtual-dtor
--Wplacement-new=1
--Wvirtual-inheritance
 -ffp-contract=off
 -pipe
 -msse4.2
 -mpopcnt
--fno-diagnostics-color
+-fno-color-diagnostics
 -O0
 -gdwarf-4
 -g3
@@ -16,15 +14,11 @@
 -Wextra
 -Wwrite-strings
 -Wno-unused-parameter
--Wshadow
--Wno-misleading-indentation
+-Wshadow-field-in-constructor
+-Wshadow-uncaptured-local
+-Wno-ordered-compare-function-pointers
 -Wenum-conversion
--Walloc-zero
--Wduplicated-branches
--Wduplicated-cond
--Wstringop-overflow=4
--Wattribute-alias=2
--Wlogical-op
+-Wimplicit-fallthrough
 -Werror
 -DTOOLS_ENABLED
 -DDEBUG_ENABLED

and gcc:

g++ -o bin/obj/main/main.linuxbsd.editor.dev.x86_64.o -c -std=gnu++17 -fno-exceptions -Wctor-dtor-privacy -Wnon-virtual-dtor -Wplacement-new=1 -Wvirtual-inheritance -ffp-contract=off -pipe -msse4.2 -mpopcnt -fno-diagnostics-color -O0 -gdwarf-4 -g3 -Wall -Wextra -Wwrite-strings -Wno-unused-parameter -Wshadow -Wno-misleading-indentation -Wenum-conversion -Walloc-zero -Wduplicated-branches -Wduplicated-cond -Wstringop-overflow=4 -Wattribute-alias=2 -Wlogical-op -Werror -DTOOLS_ENABLED -DDEBUG_ENABLED -DDEV_ENABLED -DENGINE_UPDATE_CHECK_ENABLED -DNO_EDITOR_SPLASH -DSTRICT_CHECKS -DSCU_BUILD_ENABLED -DSOWRAP_ENABLED -DTOUCH_ENABLED -DFONTCONFIG_ENABLED -DALSA_ENABLED -DALSAMIDI_ENABLED -DPULSEAUDIO_ENABLED -D_REENTRANT -DDBUS_ENABLED -DSPEECHD_ENABLED -DXKB_ENABLED -DUDEV_ENABLED -DSDL_ENABLED -DLINUXBSD_ENABLED -DUNIX_ENABLED -D_FILE_OFFSET_BITS=64 -DX11_ENABLED -DLIBDECOR_ENABLED -DWAYLAND_ENABLED -DACCESSKIT_DYNAMIC -DACCESSKIT_ENABLED -DVULKAN_ENABLED -DRD_ENABLED -DGLES3_ENABLED -DCRASH_HANDLER_ENABLED -D_LIBCPP_REMOVE_TRANSITIVE_INCLUDES -DMINIZIP_ENABLED -DBROTLI_ENABLED -DOVERRIDE_ENABLED -DOVERRIDE_PATH_ENABLED -DTHREADS_ENABLED -DCLIPPER2_ENABLED -DZLIB_DEBUG -DZSTD_STATIC_LINKING_ONLY -DUSE_VOLK -DVK_USE_PLATFORM_XLIB_KHR -DVK_USE_PLATFORM_WAYLAND_KHR -DGLAD_ENABLED -DEGL_ENABLED -DTESTS_ENABLED -DTESTS_ENABLED -Ithirdparty/freetype/include -Ithirdparty/libpng -Ithirdparty/glad -Ithirdparty/volk -Ithirdparty/vulkan -Ithirdparty/vulkan/include -Ithirdparty/accesskit/include -Ithirdparty/zstd -Ithirdparty/zlib -Ithirdparty/clipper2/include -Ithirdparty/brotli/include -Ithirdparty/linuxbsd_headers/libdecor-0 -Ithirdparty/linuxbsd_headers/wayland -Ithirdparty/linuxbsd_headers -Iplatform/linuxbsd -I. main/main.cpp

Edit: Tested further with warnings=no, there the times are similar with GCC and Clang (ca. 1m30s for both).
So some of the GCC warnings enabled with warnings=extra seem to make things hard for clang-tidy.

This shouldn't be a blocker for this PR.

Comment on lines +120 to +132
# Keep in sync with static_checks.yml
- name: Get changed files
if: matrix.clang-tidy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
files=$(git diff-tree --no-commit-id --name-only -r HEAD^1..HEAD 2> /dev/null || true)
elif [ "${{ github.event_name }}" == "push" -a "${{ github.event.forced }}" == "false" -a "${{ github.event.created }}" == "false" ]; then
files=$(git diff-tree --no-commit-id --name-only -r ${{ github.event.before }}..${{ github.event.after }} 2> /dev/null || true)
fi
files=$(echo "$files" | xargs -I {} sh -c 'echo "\"./{}\""' | tr '\n' ' ')
echo "CHANGED_FILES=$files" >> $GITHUB_ENV

@akien-mga akien-mga Feb 3, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So now that we have compiledb_gen_only (once you rebase on latest master), maybe the clang-tidy job could be moved to static_checks.yml?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would require the static checks pipeline to install build tools, I think. Another reason I added it to the linux runner is that I expect it to trigger rarely (hopefully?), so normally there should be no reason to delay all the other runners for it.

@Repiteo Repiteo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A rock-solid baseline for finally integrating this into the CI proper. I'd much rather have something implemented than being stuck in this limbo of waiting for an "ideal" integration (my b). After one nitpick is dealt with, this is good to go!

Comment thread .github/workflows/linux_builds.yml Outdated
Comment thread .github/workflows/linux_builds.yml Outdated
@Repiteo Repiteo modified the milestones: 4.x, 4.7 Feb 19, 2026
For now, it's integrated into core, main and scene only.
Fix a few superficial clang-tidy failures.
@Repiteo Repiteo merged commit 83f3023 into godotengine:master Feb 20, 2026
20 checks passed
@Repiteo

Repiteo commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment