DNDSR 0.2.1
Distributed Numeric Data Structure for CFV
Loading...
Searching...
No Matches
clang-tidy Cleanup Plan

Living document driving the check-by-check cleanup of the DNDSR C++ tree. Each "pass" below is a unit of work resulting in one commit; each bucketed check is addressed to completion before the next starts.

Driver: scripts/run_clang_tidy.py. Config: /.clang-tidy (reporting) and /.clang-tidy-fix (auto-fix profile). See docs/guides/style_guide.md for usage.

Status snapshot

  • Current module: DNDS. All planned passes complete. Pass 6 (special-member-functions), pass 7 (macro-usage), pass 8 (redundant-casting), pass 9 (member-init), pass 10 (nullptr), pass 11 (emplace), pass 12 (equals-default), pass 13 (qualified-auto), pass 14 (named-parameter), pass 15 (simplify-boolean), pass 16 (unnecessary-value-param), pass 17 (loop-convert), pass 18 (prefer-member-init), pass 19 (cstyle-cast), pass 20 (non-const-global — KA disable), pass 21 (c-arrays), pass 22 (unhandled-self-assign), pass 23 (branch-clone), pass 24 (implicit-widening), pass 25 (rvalue-ref-not-moved), pass 26 (long-tail sweep).
  • Latest total: 1 diagnostic (a clang-diagnostic-error from omp.h inside Eigen's PCH — unrelated to DNDS code; cannot be fixed without installing libomp-<v>-dev matched to the clang-tidy / clangd binary version).
  • Original baseline: 24 597 diagnostics across 51 distinct checks (captured in build/clang-tidy-logs/baseline-dnds.txt).
  • End-state delta: –99.996 % (or "effectively zero") on total diagnostics; 51 → 1 distinct checks.

Summary counts after each pass (DNDS only, all-TU):

After Total Distinct Delta
Baseline 24 597 51
Pass 1 (macro-parentheses NOLINT) 24 067 50 −530
Pass 2 (nodiscard –fix) 22 495 49 −1 572
Pass 3 (init-variables –fix) 21 297 49 −1 198
Pass 4 (missing-std-forward disable + YAML trap) 20 794 47 −503
Pass 5 (reserved-identifier rename) 19 390 46 −1 404
KA bucket disables 7 341 35 −12 049
Pass 6 (special-member-functions) 5 691 34 −1 650
Pass 7 (macro-usage disable) 3 581 33 −2 110
Pass 8 (redundant-casting –fix) 2 805 32 −776
Pass 9 (member-init –fix) 2 330 31 −475
Pass 10 (use-nullptr –fix) 2 099 30 −231
Pass 11 (use-emplace –fix) 1 987 29 −112
Pass 12 (use-equals-default –fix) 1 928 28 −59
Pass 13 (qualified-auto –fix) 1 876 27 −52
Pass 14 (named-parameter –fix) 1 654 26 −222
Pass 15 (simplify-boolean –fix + manual) 1 499 25 −155
Pass 16 (unnecessary-value-param) 1 180 24 −319
Pass 17 (loop-convert –fix + NOLINT) 1 031 23 −149
Pass 18 (prefer-member-initializer –fix) 971 23 −60
Pass 19 (cstyle-cast manual) 847 22 −124
Pass 20 (non-const-global disable) 523 21 −324
Pass 21 (avoid-c-arrays) 455 20 −68
Pass 22 (unhandled-self-assign) 402 19 −53
Pass 23 (branch-clone NOLINTBEGIN/END) 80 18 −322
Pass 24 (implicit-widening NOLINT) 14 14 −66
Pass 25 (rvalue-ref move) 13 13 −1
Pass 26 (long-tail sweep) 1 1 −12

Table of contents

  1. Scope and ground rules
  2. Baseline — DNDS
  3. Triage table
  4. Pass log
    1. Pass 1 — bugprone-macro-parentheses
    2. Pass 2 — modernize-use-nodiscard
    3. Pass 3 — cppcoreguidelines-init-variables
    4. Pass 4 — cppcoreguidelines-missing-std-forward
    5. Pass 5 — bugprone-reserved-identifier
    6. Pass 6 — cppcoreguidelines-special-member-functions
    7. Pass 7 — cppcoreguidelines-macro-usage
    8. Pass 8 — readability-redundant-casting
    9. Pass 9 — cppcoreguidelines-pro-type-member-init
    10. Pass 10 — modernize-use-nullptr
    11. Pass 11 — modernize-use-emplace
    12. Pass 12 — modernize-use-equals-default
    13. Pass 13 — readability-qualified-auto
    14. Pass 14 — readability-named-parameter
    15. Pass 15 — readability-simplify-boolean-expr
    16. Pass 16 — performance-unnecessary-value-param
    17. Pass 17 — modernize-loop-convert
    18. Pass 18 — cppcoreguidelines-prefer-member-initializer
    19. Pass 19 — cppcoreguidelines-pro-type-cstyle-cast
    20. Pass 20 — cppcoreguidelines-avoid-non-const-global-variables
    21. Pass 21 — cppcoreguidelines-avoid-c-arrays
    22. Pass 22 — bugprone-unhandled-self-assignment
    23. Pass 23 — bugprone-branch-clone
    24. Pass 24 — bugprone-implicit-widening-of-multiplication-result
    25. Pass 25 — cppcoreguidelines-rvalue-reference-param-not-moved
    26. Pass 26 — long-tail sweep
  5. Disables applied
  6. NOLINT markers in the tree
  7. Next modules

Scope and ground rules

  1. One check per commit. Mixing checks makes review impossible and regressions hard to bisect.
  2. Triage first, fix second. Before a pass starts, decide whether the check fits the project: Keep & fix, Keep & accept (add to disables), Keep but silence locally (per-site // NOLINT(...)), or Re-read later.
  3. Verify after every pass. cmake --build build -j32 must succeed. Relevant ctests (currently ‘ctest -R ’^dnds_') must pass. scripts/run_clang_format.py –check <scope>must pass.
  4. **Spot-check every auto-fix.** After any–fixrun, sample at least a handful of changed sites (random spread, not just the first) before committing.
  5. **Never skip the pre-commit hook.**
  6. **No CUDA TUs.**.cufiles are excluded by the driver (nvcc flags incompile_commands.jsonbreak clang's CUDA frontend). CUDA-included headers are still tidied via their.cpp` includers.

Baseline — DNDS

Captured with:

scripts/run_clang_tidy.py --summary --top-checks 50 DNDS \
> build/clang-tidy-logs/baseline-dnds.txt

Top-20 (after f1fb769 / 2d5257c, with the summary regex fixed, 68 TUs, 24 597 total diagnostics across 51 distinct checks):

# Check Hits
1 cppcoreguidelines-non-private-member-variables-in-classes 3 412
2 cppcoreguidelines-avoid-magic-numbers 2 690
3 cppcoreguidelines-pro-bounds-pointer-arithmetic 2 119
4 cppcoreguidelines-macro-usage 2 110
5 cppcoreguidelines-special-member-functions 1 645
6 modernize-use-nodiscard 1 572
7 bugprone-reserved-identifier 1 404
8 cppcoreguidelines-pro-type-vararg 1 402
9 cppcoreguidelines-init-variables 1 198
10 cppcoreguidelines-pro-type-reinterpret-cast 842
11 readability-redundant-casting 776
12 readability-redundant-access-specifiers 537
13 bugprone-macro-parentheses 530
14 cppcoreguidelines-missing-std-forward 503
15 cppcoreguidelines-pro-type-member-init 475
16 cppcoreguidelines-pro-type-const-cast 371
17 cppcoreguidelines-pro-bounds-array-to-pointer-decay 334
18 cppcoreguidelines-avoid-non-const-global-variables 324
19 bugprone-branch-clone 322
20 performance-unnecessary-value-param 319

‍The summary regex originally matched bracketed tokens like [[nodiscard]], <loc>, <pos>, <name> inside clang-tidy note lines, inflating the modernize-use-nodiscard row to 3 621 and adding four phantom "checks". The regex was tightened (require a - or . inside the name) before the table above was captured. Delta between the two runs: 3 740 spurious hits removed, real totals unchanged.

Goal: after all passes in this plan, the top-20 should drop by more than 50 % of total volume, with the "Keep & fix" rows going to near-zero.

Triage table

Bucket legend:

  • KFKeep & fix. Schedule a pass.
  • KAKeep & accept. Add to .clang-tidy disables.
  • KSKeep but silence locally at specific call sites with // NOLINT(check-name) + a one-line reason.
  • RLRe-read later. Decide after noisy checks are drained.
Check Bucket Rationale
cppcoreguidelines-non-private-member-variables-in-classes KA Project uses struct with public fields as data bags extensively (Array storage, MPI views, config sections). Enforcing private+getters would be a massive architectural change with no safety gain at our scale.
cppcoreguidelines-avoid-magic-numbers KA Overlaps with already-disabled readability-magic-numbers. Identical hit set, identical reasoning.
cppcoreguidelines-pro-bounds-pointer-arithmetic KA CSR storage and MPI byte buffers are fundamentally pointer-arithmetic. Fixing means migrating to std::span/gsl::span — a design move, not a tidy pass.
cppcoreguidelines-macro-usage RL Project uses macros for device portability, Eigen workarounds, config registration. Load-bearing. Revisit once other noise is gone.
cppcoreguidelines-special-member-functions KF Missing rule-of-five declarations. Can catch real bugs around implicit moves/copies. Pass 6.
modernize-use-nodiscard KF Mechanical, mostly auto-fixable. Spot-check needed (some functions are legitimately called for side effects). Pass 2.
bugprone-reserved-identifier KF _Tp, __DNDS_str, etc. are reserved-name patterns. Mechanical rename, not auto-fixable across TUs. Pass 5.
cppcoreguidelines-pro-type-vararg KA MPI and printf-family APIs require varargs. Silencing globally is correct.
cppcoreguidelines-init-variables KF Mechanical, mostly auto-fixable. Spot-check for cases where default-init is intentional (e.g. int rank; right before MPI_Comm_rank). Pass 3.
cppcoreguidelines-pro-type-reinterpret-cast KA Used for MPI byte buffers and serialization. No idiomatic replacement.
readability-redundant-casting RL Some are style-only, some indicate real type drift. Revisit after pass 7.
readability-redundant-access-specifiers KA Project explicitly repeats public: for readability in large classes. Documented convention.
bugprone-macro-parentheses KF Cheap, mechanical, catches real macro-expansion bugs. Some hits are design-intentional (token-paste operands, argument types) and need NOLINT. Pass 1.
cppcoreguidelines-missing-std-forward KF Mechanical, auto-fixable. Pass 4.
cppcoreguidelines-pro-type-member-init RL Overlaps with cppcoreguidelines-init-variables (pass 3) on members. Revisit delta.
cppcoreguidelines-pro-type-const-cast KA Needed for interop with C APIs (MPI, CGNS, HDF5) that take non-const pointers.
cppcoreguidelines-pro-bounds-array-to-pointer-decay KA Same reasoning as pointer-arithmetic.
cppcoreguidelines-avoid-non-const-global-variables RL Globals for signal handlers, log streams, MPI info are intentional; there may be stragglers worth trimming. Revisit after pass 2.
bugprone-branch-clone RL Sometimes a real duplication, sometimes stylistic (exhaustive if/else cascades for enum handling). Needs eyes.
performance-unnecessary-value-param KF candidate later Would flag things like ssp<T> taken by value where const-ref or move would do. Needs care — some by-value params are deliberate (MPI type, POD). Defer until pass 6 is done.

Pass log

Each pass below records: objective, scope, commands, sample reviews, verification (build + any ctests), commit hash, post-pass delta.

Pass 1 — bugprone-macro-parentheses

Outcome. 530 hits → 0. Total diagnostics 24 597 → 24 067.

All 530 hits collapsed to 17 unique source locations in 3 files:

File Lines (col) Distinct sites Dupes per site
src/DNDS/Defines.hpp 86:28, 88:39, 93:28, 95:39 4 66
src/DNDS/ArrayDOF.hpp 68..79:5 12 22
src/DNDS/Config/ConfigParam.hpp 700:60 1 2

Decision. All 17 are false positives: every flagged token is a C++ type name or storage-class specifier in a context where parenthesization is not valid syntax:

Fix. Three NOLINTBEGIN / NOLINTEND pairs around the macro definitions, each with a one-sentence rationale comment. 13 lines of comments replace 530 diagnostics.

Verification. cmake --build build -t dnds -j32 succeeds; the summary drop of 530 exactly matches the decrement in the total (no secondary effects).

Commit: see git log -- docs/dev/clang_tidy_plan.md.

Pass 2 — modernize-use-nodiscard

Outcome. 1 572 hits → 0. Total diagnostics 24 067 → 22 495.

Method. Single-check override config at /tmp/pass2.clang-tidy:

Checks: "-*,modernize-use-nodiscard"
WarningsAsErrors: ''
HeaderFilterRegex: '.*/DNDSR/(src|app|test/cpp)/.*'
ExtraArgs: [-UDNDS_USE_OMP, -Wno-unknown-warning-option, -Wno-unused-command-line-argument]
FormatStyle: file

Driven by scripts/run_clang_tidy.py --fix --config-file /tmp/pass2.clang-tidy DNDS.

Incident & fix. The first --fix attempt ran with 64 parallel workers and produced syntax-error corruption in Vector.hpp, Array.hpp, ArrayPair.hpp, and several other headers because the parallel workers race when the same header is edited from multiple TUs (observed: interleaved [[nodiscard]] insertions in mid-token positions).

Reverted via git checkout HEAD -- src/DNDS/ and patched the driver to force jobs=1 whenever --fix is set (with --unsafe-parallel-fix escape hatch). Serialized run completed cleanly. Committed as a separate fix(tooling) commit so the pass's diff stays pure.

Diff footprint. 11 files, 32 line-replacements (one [[nodiscard]] prefix each). 1 572 repeated hits collapsed to 32 unique decls because each header declaration is reported once per including TU.

Sample review. 4 spot-checks:

  • Array.hpp:570 at() const — const getter, value return.
  • ArrayBasic.hpp:423 at_compressed(...) const with DNDS_DEVICE_CALLABLE prefix — attribute order is valid.
  • EigenUtil.hpp:278..294 rows()/cols()/size() constEigen dimension wrappers, pure reads.
  • SerializerH5.cpp:180 get_indent() const — local helper, string-builder, no side effects.

All good; no call sites in DNDS were found that call these functions for side effects.

Verification. cmake --build build -t dnds --clean-first -j32 succeeds; euler target builds. Pre-commit clang-format ran on 11 modified files, no drift.

Commits: 7502b8d (driver serialize-on-fix) + pass 2 commit.

Pass 3 — cppcoreguidelines-init-variables

Outcome. 1 198 hits → 0. Total diagnostics 22 495 → 21 297.

Method. Same recipe as Pass 2; single-check config at /tmp/pass3.clang-tidy, --fix, serialized. One full run fixed all 1 198 hits in one shot (no stragglers).

Diff footprint. 8 files, ~30 line-replacements. Most are the classic "declare-then-immediately-MPI-writes" pattern; int x; MPI_*(&x) now int x = 0; MPI_*(&x).

Sample review found two corrections needed:

  1. ArrayTransformer.hpp:838,867 — clang-tidy initialised MPI_Datatype dtype to nullptr. That's only valid on MPI implementations where MPI_Datatype is a pointer typedef (OpenMPI). MPICH defines it as int, so nullptr would not compile. Manually corrected to MPI_DATATYPE_NULL, the canonical sentinel that works on both.
  2. ArrayDOF_op.hxx — clang-tidy inserted #include <math.h> and initialised real sqrSumAll = NAN;. The sibling function on line 228 initialises the same variable to 0. Corrected to match the sibling and removed the unnecessary include.

Verification. cmake --build build -t dnds -j32 succeeds after both corrections. Pre-commit clang-format re-ran, no drift.

Lesson learned. cppcoreguidelines-init-variables with --fix picks unusual sentinel values (nullptr based on typedef, NAN for floats). Always review auto-fix output for semantic appropriateness, not just build success.

Commit: see git log.

Pass 4 — cppcoreguidelines-missing-std-forward

Outcome. 503 hits → 0, by reclassifying as KA (keep & accept) and adding to .clang-tidy disables.

Method. Single-check --fix attempt produced 0 edits: this check does not implement auto-fix. Switched to manual review.

Sample analysis (all 9 unique sites in DNDS):

  1. Array.hpp:477Resize(index, TFRowSize&& FRowSize). FRowSize is a functor called in-place. Forwarding-ref was chosen only to accept both lvalue and rvalue callables; std::forward on a functor that the body keeps calling directly would move-from on first call and break subsequent ones.
  2. Array.hpp:723ResizeRowsAndCompress(TRowSizeFunc&&). Same pattern (functor called inside a loop).
  3. ArrayPair.hpp:249runFunctionAppendedIndex(index, TF&& F). Same pattern (F(*father, i) inside the body).
  4. ArrayEigenUniMatrixBatch.hpp:110Resize(..., TFRowSize&& rsf). Functor called twice inside a lambda capturing it by reference.
  5. Defines.hpp:834 (×2 columns) — hash functor operator()(TBegin&& begin, TEnd&& end). Iterators used for random access; no forwarding semantics apply.
  6. IndexMapping.hpp:215OffsetAscendIndexMapping(...) taking TpullSet&& pullingIndexGlobal. The body mutates the collection in place (sort, unique, erase, shrink_to_fit) but never moves it elsewhere. Should be TpullSet&, but the author's commented-out // std::forward<...>(...); // might delete shows they considered it and decided against.
  7. IndexMapping.hpp:298-299 — analogous ctor overload taking two collections, used read-only.

Decision. All nine sites are either functor-called-in-place (cannot be forwarded without breaking repeated calls) or collection-used-by-reference (should be T&/const T&, not T&&). The latter is an API change that ripples through call sites and is outside the scope of "tidy passes." The check cannot distinguish legitimate forwarding-template usage from these patterns, so it produces a steady stream of false positives.

Action. Bucket moved from KF → KA. Added to .clang-tidy disables; Checks list now has an -cppcoreguidelines-missing-std-forward line.

Secondary fix: YAML folded-scalar trap. During this pass I discovered that rationale comments placed inside the Checks: > folded scalar are parsed as text (YAML # is only a comment at line start, not inside a folded block), which silently concatenated my commentary into a malformed check name and the subsequent disables were ignored. Fixed by moving all rationale comments into the file header as a table; the Checks: block is kept comment-free. A warning note was added to the header explaining this.

Commit: see git log.

Pass 5 — bugprone-reserved-identifier

Outcome. 1 404 hits → 0. Total diagnostics 20 794 → 19 390.

Method. Manual; this check has no auto-fix. The 1 404 hits collapsed to ~25 unique identifiers, each flagged because it starts with __ or _[A-Z] or contains __ (all reserved-name patterns per [lex.name]/3.2).

Renames applied (leading underscores dropped):

Before After Notes
_Tp Tp Template parameter in 2 sites (Defines.hpp).
__DNDS_str DNDS_str Token-stringize helper macro.
__DNDS__json_to_config DNDS_json_to_config Both leading and internal __ removed.
__DNDSToMPITypeInt, __DNDSToMPITypeFloat drop leading __.
__EndTimerType EndTimerType Timer callback type.
__InSituPackStartPull, __InSituPackStartPush drop leading __.
__OneMatGetRowSize OneMatGetRowSize Template helper.
__ReadSerializerData, __ReadSerializerDataAndPropagateOffset, __ReadSerializerStructuralAndResolveDataOffset, __WriteSerializerData drop leading __. Serializer internals.
__Row_size Row_size Template metafunction.
__p_indices p_indices Helper in bind module.
__start_timer, __stop_timer start_timer, stop_timer Timer API.
__pybind11_callBind*s_rowsizes_sequence (×8) drop leading __. Our own template helpers that happened to live next to pybind11 code.
__EigenPCH, __ExprtkPCH EigenPCH_tag, ExprtkPCH_tag Module-tag strings; avoided colliding with the class / filename.

Collision-handled:

Before After Rationale
__size, __offset (SerializerBase.hpp:40) sz, ofs Ctor params; size/offset have 521 / 302 existing uses.
_GetDataLayout (ArrayBasic.hpp / Array.hpp) ComputeDataLayout GetDataLayout already exists as a different member.

Verification. cmake --build build -t dnds -j32 succeeds; cmake --build build -t euler -j32 succeeds (catches cross-module consumers in Euler/CFV); clang-tidy re-summary shows 0 hits for bugprone-reserved-identifier.

Lesson. A plain leading-underscore strip is not enough when the identifier also has an internal __; re-running the check after each bulk sed pass catches the residue quickly.

Commit: see git log.

Pass 6 — cppcoreguidelines-special-member-functions

Outcome. 1 645 → 0. Completed as one commit (1880d48).

Per-class audit. The 1 645 warnings collapsed to ~20 distinct class declarations, each bucketed into one of four categories with an explicit rule-of-five closure:

  1. Value-semantic classes (members are all shared_ptr, host_device_vector, POD, or std::vector): add = default move ctor, move assign, and destructor alongside the existing custom copy. Default move is a shallow transfer of the shared handles — correct and observably identical to "copy source + reset source" on the moved-from side. Classes: Array, ArrayAdjacency, ArrayDof, ArrayEigenMatrix, ArrayEigenMatrixBatch, ArrayEigenUniMatrixBatch, ArrayEigenVector, ArrayTransformer, ParArray, AdjacencyRow, RowView, EmptyNoDefault, host_device_vector_r0, host_device_vector_r1, and their nested iterator classes.
  2. Polymorphic RAII bases (virtual dtor, owns file handle / MPI handle / opaque exprtk pointer): = delete copy and move to prevent slicing and double-close. Classes: SerializerBase, SerializerJSON, SerializerH5, DeviceStorageBase, DeviceHostSingleAllocationBase, DeviceHostSingleAllocationDirect, ExprtkWrapperEvaluator.
  3. Classic singletons (old pre-C++11 private-unimplemented idiom): replace with = delete copy/move + = default dtor. Classes: CommStrategy, MPIBufferHandler, ResourceRecycler, PerformanceTimer.
  4. Resource-registry holders (register this with ResourceRecycler by raw pointer): = delete copy/move to prevent registering the same this twice. Classes: MPIReqHolder, MPITypePairHolder.

Every declaration carries a one-line rationale comment explaining which bucket the class falls in and why the chosen semantics are correct.

Pass 7 — cppcoreguidelines-macro-usage

Outcome. 2 110 → 0. Config-only disable commit (9c71e4b).

All 41 distinct macros in DNDS are legitimate uses that a constexpr template function cannot express:

  • Assertions / checks capturing __FILE__ / __LINE__ (DNDS_assert*, DNDS_check_throw*, DNDS_HD_assert*).
  • Code-generation DSLs that declare class members, static data, or JSON binding glue (DNDS_DECLARE_CONFIG, DNDS_FIELD, DNDS_json_to_config, DNDS_NLOHMANN_DEFINE_*, pybind11_bind_*, DNDS_DEVICE_TRIVIAL_COPY_DEFINE*).
  • Platform probes / define-before-include switches (MPICH_SKIP_MPICXX, OMPI_SKIP_MPICXX, EIGEN_DONT_PARALLELIZE).
  • Intrinsic / attribute wrappers (DNDS_likely, DNDS_unlikely, DNDS_FORCEINLINE, DISABLE_WARNING via _Pragma).
  • CMake-injected constants (DNDS_VERSION_STRING).

The full list is in the .clang-tidy header disables table.

Pass 8 — readability-redundant-casting

Outcome. 776 → 0 via single-check --fix run (742119d).

Three patterns auto-fixed:

  • MPI_Datatype(MPI_FLOAT) etc. (OpenMPI constants are already MPI_Datatype, so the functional cast is a no-op) — 11 sites in MPI.hpp.
  • reinterpret_cast<uint8_t *>(uint8_t *) identity casts in Device/DeviceStorage.cpp — 3 sites.
  • index(nSend) where nSend is already index in ArrayTransformer.hpp.

Pass 9 — cppcoreguidelines-pro-type-member-init

Outcome. 475 → 0 via --fix + 1 manual fix (173ad1a).

Raw class members (index _size, rowsize Row_size, MPI_Aint pushSendSize, ConfigTypeTag typeTag, tStart array) gained {} default initializers. Local std::array scratch buffers in MPI.cpp, SerializerH5.cpp, Defines.cpp, ArrayEigenMatrix.hpp, ArrayTransformer.hpp zero-init'd the same way.

Manual fix: auto-fix emitted struct winsize w { }; on two lines; reformatted inline to struct winsize w{};.

Pass 10 — modernize-use-nullptr

Outcome. 231 → 0 via --fix (6c494f4).

(T *)(NULL)(T *)nullptr in the past-the-end row inquiry (ArrayBasic.hpp), getenv() comparisons (MPI.hpp, MPI.cpp), and HDF5 handle probes (SerializerH5.cpp).

Pass 11 — modernize-use-emplace

Outcome. 112 → 0 via --fix (56c43af).

push_back(std::make_pair(r, dtype))emplace_back(r, dtype) in MPI type-pair vector appends (2 sites in ArrayTransformer.hpp) and HDF5 dimension vectors (SerializerH5.cpp), plus push_back(std::string(...))emplace_back(...) in MPI.cpp / MPI_bind.cpp.

Pass 12 — modernize-use-equals-default

Outcome. 59 → 0 via --fix (9231ae6).

Two sites (duplicated across TUs): DeviceHostSingleAllocationBase::~DeviceHostSingleAllocationBase() {} and DeviceHostSingleAllocationDirect::~DeviceHostSingleAllocationDirect() override {}= default.

Pass 13 — readability-qualified-auto

Outcome. 52 → 0 via --fix (7586d2f).

auto ptr = reinterpret_cast<T*>(...)auto *ptr = ... in pybind11 binding helpers; for (auto &[k, v] : map.items())for (const auto &[k, v] : map.items()) in SerializerJSON.cpp.

Pass 14 — readability-named-parameter

Outcome. 222 → 0 via --fix (0bf9edd).

Added /*unused*/ on tag-dispatch parameters (std::index_sequence<Is...> /*unused*/) in the pybind11 binding machinery.

Pass 15 — readability-simplify-boolean-expr

Outcome. 155 → 0 via --fix + 1 manual (e07c315).

!(a && b) / !(a || b) DeMorgan expansions in ArrayDOF.hpp (SFINAE enable_if), ArrayDOF_bind.hpp (if constexpr), EigenUtil.hpp (ternary condition), and Defines.hpp::checkedIndexTo32 (manual — auto-fix emitted 66 duplicates into header-include paths).

Pass 16 — performance-unnecessary-value-param

Outcome. 319 → 0 via --fix + manual sed (b2019a7).

py::buffer row by value → const py::buffer &row in 15 pybind11 setitem / operator overloads across the 5 _bind.hpp headers. Two additional sites in Serializer_bind.hpp (py::object options_in) and MPI_bind.cpp (Allreduce py_sendbuf / py_recvbuf).

Pass 17 — modernize-loop-convert

Outcome. 149 → 0 via --fix + NOLINTBEGIN/END (9a88b36).

Auto-fix converted two index-based loops over std::vector<index> in ArrayRedistributor.hpp to range-based form. One site in Array_bind.hpp (for (ssize_t i = 0; i < pullIndexGlobal.size(); i++) pullIndexVec.push_back(pullIndexGlobal.at(i));) carries NOLINTBEGIN / NOLINTEND — pybind11 array_t iterators yield pybind11::handle, not long, and the explicit index-based form is required for the numpy-to-long conversion.

A plain NOLINTNEXTLINE is insufficient: --fix rewrites the for line itself, erasing the preceding comment. Block-form NOLINT survives.

Pass 18 — cppcoreguidelines-prefer-member-initializer

Outcome. 60 → 0 via --fix (a3cb4a5).

One unique site: MPIInfo::MPIInfo(MPI_Comm ncomm) — moved comm = ncomm from the body into the member-initializer list.

Pass 19 — cppcoreguidelines-pro-type-cstyle-cast

Outcome. 124 → 0 via 6 manual edits (226ead9).

Four sites in SerializerH5.cpp / SerializerJSON.cpp: (ssp<tValue> *)(pth_2_ssp[refPath]) → explicit reinterpret_cast<ssp<tValue> *>(...) on the type-erased dedup registry (matches the author TODO).

Two sites in MPI.hpp: MPI_IN_PLACE expands to the OpenMPI-defined ((void *)1) sentinel. NOLINTNEXTLINE placed immediately above the offending Allreduce(...) call — multi- line rationale comments between the NOLINT and the offending line break the suppression (same trap as Pass 17).

Pass 20 — cppcoreguidelines-avoid-non-const-global-variables

Outcome. 324 → 0 via config disable (c407cff).

Every global mutable in DNDS is intentional and cannot be made const, thread_local, or class-scoped without wider redesign: logStream, useCout, outputDelim, HDF_mutex, isDebugging, EigenPCH_tag, ExprtkPCH_tag. Full rationale in the .clang-tidy header table.

Pass 21 — cppcoreguidelines-avoid-c-arrays

Outcome. 68 → 0 via 2 manual edits (1d60a57).

Two sites, both stack scratch buffers for printf-family calls:

  • Errors.hpp::genFatalErrorMessage: char format_buf[1024*512]std::array<char, 1024*512> format_buf{}.
  • SerializerFactory.hpp: char BUF[512]std::array<char, 512>.

Pass 22 — bugprone-unhandled-self-assignment

Outcome. 53 → 0 via 1 manual edit (dd31508).

AdjacencyRow::operator=(const AdjacencyRow &r) called std::copy(r.cbegin(), r.cend(), p_indices). On self-assign, source and destination ranges are fully overlapping — UB. Added if (this == &r) return; early-return guard with a comment explaining the UB.

Pass 23 — bugprone-branch-clone

Outcome. 322 → 0 via NOLINTBEGIN / NOLINTEND blocks and rationale comments (10f5305).

All 7 unique sites are intentional — the diagnostic fires where two logically distinct branches happen to produce the same code:

  • ArrayBasic.hpp, Array.hppif constexpr cascades over _dataLayout. TABLE_Fixed and TABLE_Max currently both compute iRow * _row_size_dynamic + iCol; the layouts are conceptually distinct (padded rows may diverge).
  • ArrayEigenUniMatrixBatch.hpp, ArrayEigenUniMatrixBatch_DeviceView.hpp, EigenUtil.hpp::MatrixFMTSafe — ternary for the Eigen options template parameter; both non-row-vector arms intentionally select ColMajor.
  • Config/ConfigParam.hpp — switch over ConfigTypeTag → JSON Schema type strings; several enums collapse to the same built-in (Enum→"string", ArrayOfObjects→"array", MapOfObjects→"object").

NOLINTBEGIN/END is required because clang-tidy reports the diagnostic at the first clone of the pair; NOLINTNEXTLINE on one line of the pair was insufficient.

Pass 24 — bugprone-implicit-widening-of-multiplication-result

Outcome. 66 → 0 via 1 NOLINT (583ab45).

One source site: std::array<char, 1024 * 512> in Errors.hpp. Compile-time constant 524 288 trivially fits in int32_t; the widening to size_t happens at compile time during template argument deduction. Spurious diagnostic; NOLINTNEXTLINE with rationale.

Pass 25 — cppcoreguidelines-rvalue-reference-param-not-moved

Outcome. 44 → 0 via 1 edit (6c90a62).

ArrayDofDeviceView(t_base &&base_view) : t_base(base_view) {} and the Const variant: base_view inside the body is an lvalue, so t_base(base_view) silently copied. Added std::move(base_view) on the base-init to perform the intended move.

Pass 26 — long-tail sweep

Outcome. 12 warnings across 10 distinct checks → 0 via 13 edits (0d6d0d9). Drains every remaining check to zero:

  • performance-move-const-arg (3 sites): removed no-op std::move of const py::buffer & / const py::module_ &.
  • readability-avoid-return-with-void-value (5 sites): changed pybind11 setitem helpers from auto (deduced void) to explicit void, dropped return from the wrapping lambdas.
  • readability-make-member-function-const (2 sites): made SerializerFactory::BuildSerializer / ::ModifyFilePath const.
  • bugprone-empty-catch (4 sites): four env-var parse catches in CommStrategy::CommStrategy guarded with NOLINTBEGIN/END.
  • readability-redundant-member-init: dropped : SerializerBase() in SerializerH5.hpp.
  • modernize-pass-by-value: SerializerFactory(const std::string &)SerializerFactory(std::string _type) : type(std::move(_type)) {}.
  • modernize-use-auto (2 sites): py::buffer buf = v.cast<...>()auto buf = ...; TraverseData *data = static_cast<...>auto *data.
  • readability-container-size-empty: ver.length()!ver.empty() in Defines.cpp.
  • bugprone-exception-escape: ~SerializerJSON() wraps CloseFileNonVirtual() in try/catch — destructors mustn't throw (NOLINTBEGIN/END guards the empty catch).
  • modernize-return-braced-init-list: kept return std::string(n, ch) in SerializerH5.cpp::get_indent with NOLINTNEXTLINE — brace init is ambiguous with initializer_list<char> and triggers -Wnarrowing.
  • performance-unnecessary-copy-initialization: NOLINT on T vV = v in SerializerH5.cpp (clang-tidy misses that the variable is addressed via &vV in the non-string if constexpr branch).
  • cppcoreguidelines-avoid-const-or-ref-data-members: NOLINT on H5Contents &contents in the TraverseData per-call aggregate.
  • performance-no-int-to-ptr: NOLINT on MPI_Comm(pComm) — Python side passes an opaque uintptr_t.
  • performance-inefficient-vector-operation: added pArgvOut.reserve(*pargc) before the emplace_back loop in MPI_bind.cpp.
  • bugprone-multi-level-implicit-pointer-conversion: NOLINT on H5Aread(attr_id, dtype_id, &attr_value) where attr_value is char * — HDF5 wants void *buf and explicit static_cast<void*> does not silence the check.
  • cppcoreguidelines-owning-memory (7 sites): NOLINT on shared-pointer deleter callback (DeviceStorage.cpp), opaque exprtk pointers (ExprtkWrapper.cpp), and MPI_Init argv allocation (MPI_bind.cpp) with rationale.

NOLINT placement: a repeated gotcha

Every auto-fix pass in this session re-taught the same lesson:

  • NOLINTNEXTLINE(check) applies to the line immediately following the directive. Rationale comments must come before the directive, not between it and the offending code, otherwise the NOLINT applies to the rationale line.
  • When --fix can rewrite the flagged line (e.g. modernize-loop-convert, modernize-return-braced-init-list), use NOLINTBEGIN(check) ... NOLINTEND(check) around the preserved block — the directive line survives the rewrite.
  • For switch / ?: / chained if / else if with bugprone-branch-clone, the diagnostic is reported at the first clone of the pair, which may not match the line the edit would change. NOLINTBEGIN/END is the safe form.

Disables applied

Added to .clang-tidy during this effort (all motivated by the triage table above):

Check Reason
cppcoreguidelines-missing-std-forward Functor T&& called in-place (Pass 4).
cppcoreguidelines-non-private-member-variables-in-classes Project uses struct-of-fields data bags.
cppcoreguidelines-avoid-magic-numbers Duplicate of already-disabled readability-magic-numbers.
cppcoreguidelines-pro-bounds-pointer-arithmetic CSR / MPI buffer idioms.
cppcoreguidelines-pro-bounds-array-to-pointer-decay Same.
cppcoreguidelines-pro-bounds-constant-array-index Same.
cppcoreguidelines-pro-type-vararg MPI / printf-family.
cppcoreguidelines-pro-type-reinterpret-cast MPI byte buffers, serialization.
cppcoreguidelines-pro-type-const-cast C-API interop (MPI, CGNS, HDF5).
readability-redundant-access-specifiers Repeated public: is a project convention.
modernize-use-transparent-functors Eigen expression templates break with std::less<> etc.
cppcoreguidelines-c-copy-assignment-signature Duplicate of misc-unconventional-assign-operator.
cppcoreguidelines-macro-usage All 41 DNDS macros require __FILE__/__LINE__ capture, token pasting, code generation, or define-before-include semantics (Pass 7).
cppcoreguidelines-avoid-non-const-global-variables Every DNDS global mutable (logStream, useCout, outputDelim, HDF_mutex, isDebugging, PCH tags) is intentional; redesign out of scope for a tidy pass (Pass 20).

Rationale comments live in the .clang-tidy file header, not inside the Checks: > folded scalar (see Pass 4 / YAML trap).

NOLINT markers in the tree

The tidy session left ~70 targeted NOLINT markers. All of them pair with a rationale comment. Representative breakdown:

Check Count Notable sites
bugprone-branch-clone 5 ArrayBasic.hpp, Array.hpp, Eigen options ternaries, ConfigParam.hpp switch
cppcoreguidelines-owning-memory 4 Device/DeviceStorage.cpp deleter, ExprtkWrapper.cpp opaque new, MPI_bind.cpp argv alloc
bugprone-empty-catch 5 4x CommStrategy::CommStrategy env-var parse, 1x ~SerializerJSON
modernize-loop-convert 1 Array_bind.hpp pybind11 array_t iteration
cppcoreguidelines-pro-type-cstyle-cast 2 MPI_IN_PLACE (OpenMPI (void*)1 sentinel)
bugprone-implicit-widening-of-multiplication-result 1 Errors.hpp compile-time 1024*512
modernize-return-braced-init-list 1 SerializerH5.cpp std::string(n, ch) vs initializer_list overload
performance-unnecessary-copy-initialization 2 SerializerH5.cpp T vV = v used in non-string if constexpr branch
cppcoreguidelines-avoid-const-or-ref-data-members 1 TraverseData H5Literate callback state
performance-no-int-to-ptr 1 MPI_Comm(pComm) in pybind11 ctor
bugprone-multi-level-implicit-pointer-conversion 1 H5Aread(..., &char_ptr)
bugprone-macro-parentheses 3 blocks Unparenthesizable type-name / storage-class args

Next modules

DNDS is now clean. Every check with >= 1 actionable instance has been driven to zero, either by fixing the code or by adding a rationale-commented NOLINT / .clang-tidy disable. The sole remaining diagnostic is a clang-diagnostic-error on omp.h inside Eigen's PCH, which is an Eigen-internal include path issue unrelated to DNDS source.

Repeat the recipe for the other modules in this order:

  1. src/Solver/ — small, limited blast radius; good next target.
  2. src/Geom/ — largest module; expect a new set of checks specific to mesh connectivity loops. Start with clang-analyzer-optin.cplusplus.VirtualCall per the historical note in .clang-tidy.
  3. src/CFV/ — follows Geom closely.
  4. src/Euler/, src/EulerP/ — solver layer; bugprone-branch-clone will matter here (exhaustive if / else cascades for enum handling).

The .clang-tidy disables and NOLINT placement lessons carry forward unchanged. Any new module-specific disables should be appended to the header table, not inside the Checks: block (YAML folded scalars eat # as literal text).

The .clang-tidy disables carry forward unchanged. Any new module- specific disables should be appended in the same table at the top of .clang-tidy, not inside the Checks: block.