Skip to content

Enable clang-tidy's readability-function-cognitive-complexity check #7358

Description

Diff to enable this looks something like this:

diff --git a/.clang-tidy b/.clang-tidy
index c0a984def..941f67e03 100644
--- a/.clang-tidy
+++ b/.clang-tidy
@@ -39,11 +39,16 @@ Checks: >
   -performance-no-int-to-ptr,
   portability-*,
   readability-*,
-  -readability-function-cognitive-complexity,
   -readability-identifier-length,
   -readability-avoid-nested-conditional-operator,
   -readability-convert-member-functions-to-static,
 
+CheckOptions:
+  - key: readability-function-cognitive-complexity.IgnoreMacros
+    value: 'true'
+  - key: readability-function-cognitive-complexity.Threshold
+    value: '50'
+
 WarningsAsErrors: '*'
 HeaderFilterRegex: '?!(3rdparty)'
 FormatStyle:     'file'

I think we definitely want to ignore macros, because our logging macros are measured as extremely complex but in practice don't make the functions harder to read.

The default threshold is 25, which flags a huge number of functions, and I think is a little low. I suggest we start with a higher threshold, such as 50, for at least an initial pass.

A benefit of this (beyond pure readability) should be that we improve the clang-tidy coverage for other checks - such as bugprone-unchecked-optional-access - to silently fail, missing clear errors. By simplifying functions, reducing the scope that these checks need to analyse, we should get better coverage. One frustrating niggle is that it's not clear to me what a "safe" threshold for this is - the "cognitive complexity" does not directly correspond with the flow-analysis complexity that causes these checks to fail, and we don't know what threshold the checks fail at. But these measures are likely correlated, and we can do some work to validate where certain checks are and are not being run.

Remaining work after #8504

The check is already enabled by #7995 with Threshold=50 and IgnoreMacros=true. The remaining work is to remove the baseline suppressions through behaviour-preserving refactors.

Inventory checked against main at a6fe2f2 on 2026-10-05, excluding NodeEndpoints::init_handlers, which the now-merged #8504 addresses. The post-#8504 backlog contains six top-level functions plus two nested callbacks, covered by seven suppression sites across five files. The two mechanical follow-up PRs below are open, not yet merged.

Scores below are the historical measurements at bc7f0cf from the earlier comment, not newly measured scores on the current revision. Each refactor should verify that the registration function and all extracted callbacks meet the threshold, rather than merely moving the suppression.

Baseline score Function Location Remaining approach Follow-up
277 Logging sample init_handlers samples/apps/logging/logging.cpp:593 Mechanical: extract inline endpoint handlers, preserving registration, authentication, forwarding, and handler behaviour. #8508 (open)
66 setup_basic_hooks src/node/node_state.h:3277 Mechanical: extract hook callbacks, preserving hook order, capture lifetimes, and commit/rollback behaviour. #8509 (open)
125; nested callbacks 89 and 61 initiate_join_unsafe and its response/task callbacks src/node/node_state.h:1382 Partly mechanical: callback extraction reduces the outer score, but the callbacks themselves still need decomposition of join-response handling. Not started
69 fill_range_response_from_file src/node/rpc/file_serving_handlers.h:173 Control-flow decomposition: isolate range parsing/response construction while preserving validation and HTTP semantics. Not started
90 process_command_inner src/node/rpc/frontend.h:707 Careful request-path decomposition: preserve authentication, forwarding, transaction/retry behaviour, and error paths. Not started
102 do_execute_request src/js/registry.cpp:58 Careful JS execution-path decomposition: preserve exception/timeout handling, ownership, and response semantics. Not started

Already addressed since the earlier inventory: the two unit-test suppressions (#8482), governance ack handlers (#8484), service-state handlers (#8486), proposal handlers (#8487), file-serving handler registration (#8488), and the programmability constructor (#8489). Node endpoint registration is addressed by #8504.

Actual clang-tidy 18.1.8 measurements with IgnoreMacros=true in the open follow-ups: #8508 reduces logging init_handlers from 277 to 0, with all 41 extracted handlers scoring at most 31; #8509 reduces setup_basic_hooks from 66 to 0, with all six extracted callbacks scoring at most 12. All retained registration adapters in both PRs score 0. Neither PR changes thresholds or adds complexity suppressions.

After these two PRs land, the three control-flow refactors and the partially mechanical join-response item remain for assessment.

Activity

  1. achamayou commented on Oct 1, 2026

    @achamayou
    Member

    The check was enabled in #7995 with Threshold=50 and IgnoreMacros=true, and baseline NOLINTs were added for everything above that. These are the functions currently over 50, all suppressed (16 functions, 15 NOLINT sites), measured at bc7f0cf by running only this check with the NOLINTs neutralised:

    Score Function Location
    277 init_handlers samples/apps/logging/logging.cpp:593
    266 init_handlers src/node/rpc/node_frontend.h:484
    127 init_proposals_handlers src/node/gov/handlers/proposals.h:473
    125 initiate_join_unsafe (+ nested lambdas, 89 and 61) src/node/node_state.h:1382
    109 init_service_state_handlers src/node/gov/handlers/service_state.h:422
    102 do_execute_request src/js/registry.cpp:58
    90 process_command_inner src/node/rpc/frontend.h:703
    69 init_ack_handlers src/node/gov/handlers/acks.h:28
    69 / 66 fill_range_response_from_file / init_file_serving_handlers src/node/rpc/file_serving_handlers.h:173 / :552
    66 setup_basic_hooks src/node/node_state.h:3277
    55 ProgrammabilityHandlers constructor samples/apps/programmability/programmability.cpp:281
    84 / 53 test cases (unit tests only) src/node/test/historical_queries.cpp:1293, src/indexing/test/indexing.cpp:460

    A function's score includes the bodies of lambdas defined inside it, so the init_*handlers functions are large mostly because their endpoint handlers are written inline. The two test cases are in unit tests, which CI doesn't run clang-tidy on.

  2. achamayou commented on Oct 6, 2026

    @achamayou
    Member

    Remaining work (as of 2026-10-06)

    The check is enabled by #7995 (Threshold=50, IgnoreMacros=true). The remaining work is to remove the last baseline suppressions through behaviour-preserving refactors, without raising the threshold.

    Since the first inventory, #8482, #8484, #8486, #8487, #8488, #8489, #8504, #8508, and #8509 have removed 10 of the 15 original suppression sites. Most of those removals were mechanical handler or callback extractions. Each of the four functions left needs real control-flow decomposition. They account for five suppression sites across four files: one NOLINTNEXTLINE per function, plus a NOLINTBEGIN/END around the join response callback.

    Scores were measured on main at 1688a7f with clang-tidy 18.1.8, using the repository .clang-tidy with suppressions disabled. They are unchanged from the original inventory.

    Score Function Where the complexity is Suggested decomposition Overlapping open PRs
    125 (response callback 89, task 61) NodeState::initiate_join_unsafe, src/node/node_state.h:1382 117 of 125 comes from the inline response callback and join-response task. The task alone scores 61, mainly from trusted-join completion and snapshot install (29), 4xx handling and the snapshot fetch (13), transport errors (7), and redirects (6). Move the task body into a member function, then split it by outcome: transport error, 4xx/StartupSeqnoIsOld, redirect, and trusted completion. Moving it without splitting still leaves 61. Preserve the in-flight gate reset, avoidance of NodeState::lock on the libuv thread, the by-value target_address, retryable-vs-fatal classification, and state-transition order. #8490 (draft) changes one line in the snapshot-install path.
    102 BaseDynamicJSEndpointRegistry::do_execute_request, src/js/registry.cpp:58 Conversion of the handler return value: body (47) and headers (15). The log/return-exception-details branch appears five times. Extract body, header, and status conversion helpers, plus a single exception-reporting helper. Preserve byte-identical error strings (they currently differ subtly between branches), timeout messages, extension-scope lifetimes, and the ordering of the compaction-conflict rethrow. None
    90 RpcFrontend::process_command_inner, src/node/rpc/frontend.h:707 Commit/result handling (24), redirect/forwarding (22), nine catch clauses (14), loop guards (13), and authentication/execution (13). Extract the redirect/forwarding decision and the commit-result handling, keeping the retry loop and exception-to-error mapping in place. Preserve retry semantics, Tx ownership hand-off, acquire ordering of consensus/history loads, and the abort on serialisation failure. #8190 (draft, inactive since August) changes two can_replicate() calls.
    69 fill_range_response_from_file, src/node/rpc/file_serving_handlers.h:173 Range header parsing (44). Extract a pure Range parser that returns a range or an error, which also makes the parser unit-testable. Preserve RFC 9110 suffix/open-ended semantics, overflow-safe clamping, empty-range rejection, and the error messages. #8214 (open) renames this function to a templated fill_range_response and rewrites the read path, so this refactor is best done after #8214 lands.

    Each PR should remove its suppression and confirm that the original function and every extracted helper score at most 50, rather than moving the suppression elsewhere.

  3. achamayou commented on Oct 7, 2026

    @achamayou
    Member

    Remaining work (as of 2026-10-07)

    #8511 has merged. It removed the suppression from BaseDynamicJSEndpointRegistry::do_execute_request, whose score dropped from 102 to 19; no extracted helper scores above 19. Three functions remain. They have four suppression sites across three files: one NOLINTNEXTLINE per function, plus a NOLINTBEGIN/END around the join response callback.

    The scores below are unchanged from the previous measurement, taken with clang-tidy 18.1.8 using the repository .clang-tidy with suppressions disabled. On main at 5f1a053, initiate_join_unsafe and fill_range_response_from_file are token-identical to the measured revision. process_command_inner differs only by a renamed method call, which does not affect the score.

    Score Function Where the complexity is Overlapping open PRs
    125 (response callback 89, task 61) NodeState::initiate_join_unsafe, src/node/node_state.h:1388 117 of 125 comes from the inline response callback and join-response task. The task alone scores 61, mainly from trusted-join completion and snapshot install (29), 4xx handling and the snapshot fetch (13), transport errors (7), and redirects (6). #8490 (draft) changes one line in the snapshot-install path.
    90 RpcFrontend::process_command_inner, src/node/rpc/frontend.h:710 Commit/result handling (24), redirect/forwarding (22), nine catch clauses (14), loop guards (13), and authentication/execution (13). #8190 (draft, inactive since August) changes two can_replicate() calls.
    69 fill_range_response_from_file, src/node/rpc/file_serving_handlers.h:173 Range header parsing (44). #8214 (open, currently conflicting with main) renames this function to a templated fill_range_response and rewrites its read path.

    Lessons from #8511 for the remaining PRs:

    • Move code verbatim: move blocks unchanged into helpers, so they are token-identical to main apart from the helper calls and return statements.
    • Preserve order and lifetimes: keep the order of side effects and the lifetimes of values exactly as they were.
    • Verify behaviour: compare tokens against main, and run a before/after behavioural comparison where practical.

    Each PR should remove its suppression and confirm that the original function and every extracted helper score at most 50, rather than moving the suppression elsewhere.

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

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions