From 369140f4cc135ac246c0b61efbf361415cde87f9 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Wed, 4 Feb 2026 15:03:00 -0800 Subject: [PATCH 1/2] :safety_vest: Enable clang-tidy & pre-commit checks --- .github/workflows/ci.yml | 12 +++++++++ .pre-commit-config.yaml | 35 ++++++++++++++++++++++++++ CMakeLists.txt | 21 ++++++++++++++++ cspell.json | 53 ++++++++++++++++++++++++++++++++++++++++ docs/doxygen.conf | 22 +++++++++++++++++ 5 files changed, 143 insertions(+) create mode 100644 .pre-commit-config.yaml create mode 100644 cspell.json create mode 100644 docs/doxygen.conf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82cb71d..2139604 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,19 @@ concurrency: cancel-in-progress: true jobs: + pre-commit: + name: ๐Ÿงน Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: sudo apt-get install -y doxygen + - uses: pre-commit/action@v3.0.1 + package_and_upload_all_check: + name: ๐Ÿ”Ž Package Validation uses: libhal/ci/.github/workflows/package_and_upload_all.yml@5.x.y with: modules_support_needed: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..1814ba5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +repos: + # General file fixes + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + args: [--fix=lf] + + # Spell checking + - repo: https://github.com/streetsidesoftware/cspell-cli + rev: v8.6.0 + hooks: + - id: cspell + files: \.(cpp|cppm|hpp|h|md)$ + + # C++ formatting with clang-format + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v18.1.3 + hooks: + - id: clang-format + types_or: [c, c++] + files: \.(cpp|cppm|hpp|h|c)$ + + # Doxygen documentation checker + - repo: local + hooks: + - id: doxygen-check + name: doxygen-check + entry: doxygen + args: [docs/doxygen.conf] + language: system + pass_filenames: false + files: \.(cpp|cppm|hpp|h)$ diff --git a/CMakeLists.txt b/CMakeLists.txt index 386789d..a227845 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,9 +23,30 @@ set(BUILD_UNIT_TESTS ON) set(RUN_UNIT_TESTS ON) set(BUILD_BENCHMARKS ON) set(RUN_BENCHMARKS OFF) +set(USE_CLANG_TIDY ON) project(async_context LANGUAGES CXX) +# ============================================================================== +# Find clang-tidy +# ============================================================================== + +if(CMAKE_CROSSCOMPILING) + message(STATUS "Cross compiling, skipping clang-tidy") +else() + if(USE_CLANG_TIDY) + find_program(CLANG_TIDY_EXE NAMES clang-tidy) + if(CLANG_TIDY_EXE) + message(STATUS "Clang-tidy found: ${CLANG_TIDY_EXE}") + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + else() + message(STATUS "Clang-tidy not found - continuing without clang-tidy") + endif() + else() + message(WARNING "Clang-tidy checks disabled") + endif() +endif() + # ============================================================================== # Library # ============================================================================== diff --git a/cspell.json b/cspell.json new file mode 100644 index 0000000..16dd5a9 --- /dev/null +++ b/cspell.json @@ -0,0 +1,53 @@ +{ + "version": "0.2", + "language": "en", + "words": [ + "cppm", + "noexcept", + "nullptr", + "constexpr", + "decltype", + "typename", + "templated", + "allocator", + "deallocate", + "deallocator", + "deallocating", + "realloc", + "inout", + "libhal", + "RAII", + "dtor", + "ctor", + "ctors", + "dtors", + "intptr", + "uintptr", + "ptrdiff", + "ifdef", + "ifndef", + "endif", + "elif", + "pragma", + "nodiscard", + "nothrow", + "initializer", + "uninitialised", + "uninitialized", + "Khalil", + "Estell", + "NOLINTNEXTLINE", + "bugprone", + ], + "ignorePaths": [ + "build/", + "*.json", + "*.bmi", + "compile_commands.json" + ], + "enableFiletypes": [ + "cpp", + "c", + "markdown" + ] +} diff --git a/docs/doxygen.conf b/docs/doxygen.conf new file mode 100644 index 0000000..e926729 --- /dev/null +++ b/docs/doxygen.conf @@ -0,0 +1,22 @@ +# Adapted from https://github.com/xtensor-stack/xtensor/blob/master/docs/Doxyfile +PROJECT_NAME = "async_context" +INPUT = ../modules +FILE_PATTERNS = *.cppm *.hpp +EXTRACT_STATIC = YES +EXCLUDE_PATTERNS = */third_party/* +HAVE_DOT = YES +OUTPUT_DIRECTORY = build +XML_OUTPUT = xml +GENERATE_LATEX = NO +GENERATE_MAN = NO +GENERATE_RTF = NO +CASE_SENSE_NAMES = NO +GENERATE_HTML = NO +GENERATE_XML = YES +RECURSIVE = YES +QUIET = YES +PREDEFINED = IN_DOXYGEN +EXCLUDE_SYMBOLS = detail +GENERATE_TREEVIEW = YES +SOURCE_BROWSER = YES +WARN_IF_UNDOCUMENTED = YES From b2142bfcc58389ea5e1ff5acfb6e5dd95cae35c5 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Wed, 4 Feb 2026 15:31:14 -0800 Subject: [PATCH 2/2] fixup clang-tidy --- .clangd | 1 - .pre-commit-config.yaml | 2 +- CMakeLists.txt | 2 +- benchmarks/benchmark.cpp | 6 ++++-- conanfile.py | 2 +- cspell.json | 8 ++++++++ modules/async_context.cppm | 17 +++++++++++++---- tests/cancel.test.cpp | 30 +++++++++++++++--------------- tests/util.cppm | 2 ++ 9 files changed, 45 insertions(+), 25 deletions(-) diff --git a/.clangd b/.clangd index cf229d1..f8ff8cd 100644 --- a/.clangd +++ b/.clangd @@ -1,4 +1,3 @@ CompileFlags: CompilationDatabase: . BuiltinHeaders: QueryDriver - diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1814ba5..a6c7d92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: # C++ formatting with clang-format - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.3 + rev: v21.1.8 hooks: - id: clang-format types_or: [c, c++] diff --git a/CMakeLists.txt b/CMakeLists.txt index a227845..f6a52a2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,7 @@ else() find_program(CLANG_TIDY_EXE NAMES clang-tidy) if(CLANG_TIDY_EXE) message(STATUS "Clang-tidy found: ${CLANG_TIDY_EXE}") - set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE}") + set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE};--config-file=${CMAKE_CURRENT_SOURCE_DIR}/.clang-tidy;--fix") else() message(STATUS "Clang-tidy not found - continuing without clang-tidy") endif() diff --git a/benchmarks/benchmark.cpp b/benchmarks/benchmark.cpp index aad9486..cc9504d 100644 --- a/benchmarks/benchmark.cpp +++ b/benchmarks/benchmark.cpp @@ -33,12 +33,14 @@ import async_context; // Quick Bench: https://quick-bench.com/ -// Compiler flags: -std=c++23 -O3 -DNDEBUG +// Compiler flags: -std=c++23 -O3 -D NDEBUG // ============================================================================ // BENCHMARKS // ============================================================================ +// NOLINTBEGIN(clang-analyzer-deadcode.DeadStores) + // ---------------------------------------------------------------------------- // 1. BASELINE: Direct returns, 3 levels deep // ---------------------------------------------------------------------------- @@ -477,4 +479,4 @@ static void bm_future_void_coroutine_context_resume(benchmark::State& state) BENCHMARK(bm_future_void_coroutine_context_resume); BENCHMARK_MAIN(); -// NOLINTEND(readability-identifier-naming) +// NOLINTEND(clang-analyzer-deadcode.DeadStores) diff --git a/conanfile.py b/conanfile.py index 1ba6b92..ad6b43a 100644 --- a/conanfile.py +++ b/conanfile.py @@ -33,7 +33,7 @@ class async_context_conan(ConanFile): description = ("Implementation of C++20 coroutines targeting embedded system by eliminating the usage of the global heap and providing a 'context' which contains a coroutine stack frame and other useful utilities for scheduling.") topics = ("async", "coroutines", "stack", "scheduling", "scheduler") settings = "compiler", "build_type", "os", "arch" - exports_sources = "modules/*", "benchmarks/*", "tests/*", "CMakeLists.txt", "LICENSE" + exports_sources = "modules/*", "benchmarks/*", "tests/*", "CMakeLists.txt", "LICENSE", ".clang-tidy" package_type = "static-library" shared = False diff --git a/cspell.json b/cspell.json index 16dd5a9..dd6f45b 100644 --- a/cspell.json +++ b/cspell.json @@ -38,6 +38,14 @@ "Estell", "NOLINTNEXTLINE", "bugprone", + "println", + "cassert", + "coro", + "chrono", + "EABI", + "NDEBUG", + "malloc", + "uptr" ], "ignorePaths": [ "build/", diff --git a/modules/async_context.cppm b/modules/async_context.cppm index d9a61a0..bd8ba0d 100644 --- a/modules/async_context.cppm +++ b/modules/async_context.cppm @@ -1591,7 +1591,7 @@ struct final_awaiter * continuation of the calling coroutine. * * @param p_completing_coroutine The coroutine handle that is completing - * @return The coroutine handle to resume next, which is a symmetryic transfer + * @return The coroutine handle to resume next, which is a symmetric transfer * from this completing coroutine to its continuation (what called it * originally). */ @@ -1643,15 +1643,19 @@ struct promise_return_base void return_value(U&& p_value) noexcept requires std::is_constructible_v { - // set future to its awaited T value + // NOLINTBEGIN(clang-analyzer-core.CallAndMessage): clang-tidy incorrectly + // assumes this pointer is uninitialized. The promise is constructed from + // the future returned by `get_return_object()`, which properly initializes + // this promise. m_future_state->template emplace(std::forward(p_value)); + // NOLINTEND(clang-analyzer-core.CallAndMessage) } /** * @brief Pointer to the future state that should be set at future * construction. */ - future_state* m_future_state; + future_state* m_future_state = nullptr; }; /** @@ -1667,14 +1671,19 @@ struct promise_return_base */ void return_void() noexcept { + // NOLINTBEGIN(clang-analyzer-core.CallAndMessage): clang-tidy incorrectly + // assumes this pointer is uninitialized. The promise is constructed from + // the future returned by `get_return_object()`, which properly initializes + // this promise. *m_future_state = std::monostate{}; + // NOLINTEND(clang-analyzer-core.CallAndMessage) } /** * @brief Pointer to the future state that should be set at future * construction. */ - future_state* m_future_state; + future_state* m_future_state = nullptr; }; /** diff --git a/tests/cancel.test.cpp b/tests/cancel.test.cpp index a996b4f..6803bd4 100644 --- a/tests/cancel.test.cpp +++ b/tests/cancel.test.cpp @@ -45,22 +45,22 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { }; { - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); auto future = c(ctx); - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); future.resume(); - expect(that % count == counter_pair{ 3, 0 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 0 }); expect(that % ends_reached == 0); expect(that % 0 < ctx.memory_used()); } // destroy future - expect(that % count == counter_pair{ 3, 3 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 3 }); expect(that % ends_reached == 0); expect(that % 0 == ctx.memory_used()); }; @@ -104,23 +104,23 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { co_return; }; - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); auto future = c(ctx); - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); future.resume(); - expect(that % count == counter_pair{ 3, 0 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 0 }); expect(that % ends_reached == 0); expect(that % 0 < ctx.memory_used()); future.cancel(); - expect(that % count == counter_pair{ 3, 3 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 3 }); expect(that % ends_reached == 0); expect(that % 0 == ctx.memory_used()); }; @@ -164,17 +164,17 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { co_return; }; - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); auto future = c(ctx); - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); future.resume(); - expect(that % count == counter_pair{ 3, 0 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 0 }); expect(that % ends_reached == 0); expect(that % 0 < ctx.memory_used()); expect(that % false == future.has_value()); @@ -182,7 +182,7 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { ctx.cancel(); - expect(that % count == counter_pair{ 3, 3 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 3 }); expect(that % ends_reached == 0); expect(that % 0 == ctx.memory_used()); expect(that % false == future.has_value()); @@ -232,12 +232,12 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { co_return; }; - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); auto future = c(ctx); - expect(that % count == counter_pair{ 0, 0 }); + expect(that % count == counter_pair{ .constructed = 0, .destructed = 0 }); expect(that % ends_reached == 0); std::println("Resume until future reaches suspension @ coroutine A"); @@ -247,7 +247,7 @@ boost::ut::suite<"cancellation_tests"> cancellation_tests = []() { << "runtime_error exception was not thrown!"; expect(that % future.done()); expect(that % not future.has_value()); - expect(that % count == counter_pair{ 3, 3 }); + expect(that % count == counter_pair{ .constructed = 3, .destructed = 3 }); expect(that % ends_reached == 0); expect(that % 0 == ctx.memory_used()); }; diff --git a/tests/util.cppm b/tests/util.cppm index 4d5c043..d9952e4 100644 --- a/tests/util.cppm +++ b/tests/util.cppm @@ -10,6 +10,7 @@ export module test_utils; import async_context; +// NOLINTBEGIN(bugprone-exception-escape) export namespace async { std::ostream& operator<<(std::ostream& out, blocked_by b) { @@ -148,3 +149,4 @@ export { std::string_view m_label; }; } +// NOLINTEND(bugprone-exception-escape)