Skip to content

BUG: Parenthesize macro arguments and replacement lists - #61

Merged
hjmjohnson merged 1 commit into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-macro-parentheses
Sep 23, 2026
Merged

hjmjohnson merged 1 commit into
InsightSoftwareConsortium:masterfrom
gdevenyi:pr/fix-macro-parentheses

Conversation

@gdevenyi

@gdevenyi gdevenyi commented Aug 15, 2026 •

Copy link
Copy Markdown

170 bugprone-macro-parentheses findings, of which this clears 157.

An unparenthesised macro argument silently takes the precedence of
whatever the caller passed. NT_FILL(..., rv) expanding to rv = ...
does the wrong thing for any caller passing an expression, and
NT_MAT33_TO_MAT44(m33, m44) expanding to m44.m[0][0] = ... breaks for
any argument that is not a plain identifier. No current call site trips
on it, which is exactly why it would otherwise be found the hard way.

Sites: the NT_FILL, NT_DCONVERT and NT_MAT* macro families in
nifti_tool.h and nifti1_tool.h, and FSL_RADIOLOGICAL in fslio.h.

Two notes on what was not taken from the automated fix:

  • clang-tidy parenthesised the dtype and stype parameters of
    NT_DCONVERT_NO_CHECKS and NT_DCONVERT_W_CHECKS. Those are type
    names, so (dtype) * pd = dptr; is a cast expression rather than a
    declaration, and the build fails outright. Those four are reverted
    and the two macros carry a NOLINT fence saying why the check cannot
    be satisfied there.

  • FSL_RADIOLOGICAL is in an installed header, so its text does change:
    -1 becomes (-1). Its value does not. Verified by compiling against
    the installed fslio.h and printing it, and by diffing gcc -dM -E over
    every installed header: that macro's replacement list is the only
    definition that differs from master.

Thirteen findings are deliberately left, all on lines the open PRs
already rewrite: the four NT_DT_* constants in each tool header and the
two NT_DCONVERT loop bounds (#22), the QSTR macro in both io files
(#22 and #24), and three in nifti_tester001.c (#22). #22 fixes this
same defect class by hand in those places; this is the remainder, and
the two do not touch a line in common.


Interface impact: none. On the union of all these changes, configured with USE_FSL_CODE=ON and USE_CIFTI_CODE=ON: all 448 exported symbols across libniftiio, libnifti2, libznz, libfslio, libnifticdf and libcifti are identical to master under nm -D --defined-only, and all ten installed headers are identical under gcc -E -P. Under gcc -dM -E one macro definition differs, intentionally and only in text: #61 makes FSL_RADIOLOGICAL read (-1) so it is safe inside an expression. Its value is still -1, checked by compiling against each installed fslio.h and printing it.

Verification. This branch: builds with gcc 16.1.1, ctest unchanged from master (2 of 345 fail on master itself in this environment; #31 and #29 each fix one). The union of all the PRs: 0 errors under both gcc 16.1.1 and clang 22.1.8, ctest 345/345 under each, and the whole suite under valgrind memcheck with --trace-children=yes gives 484 traced processes with no invalid access, no uninitialised value and no leak in any nifti binary.

Coordination. Every line of every branch was compared, whitespace-normalised, against the diffs of the open PRs (#11, #21, #22, #23, #24). Where one of those already changes a line, the line was left alone, and the few deliberate overlaps are named in the text above. What survives is 17 compiler warnings, all of them on those lines: 9 -Wsign-conversion (5 in fslio.c for #22, 2 in nifti2_io.c and 2 in nifti_tester001.c for #24) and 8 -Wcalloc-transposed-args in nifti_findhdrname and nifti_findimgname, which #11 rewrites. No formatting changes appear anywhere, to stay clear of #10 and #12.

One of a set of independent, single-purpose PRs. Each bases on master and can be merged on its own, in any order.

The full set of PRs (35)

The union of all of them is on the fork as all-changes, if you want to build and test the lot at once.

CI and build

Configuration and documentation

Defects

Warning and check classes

This was referenced Aug 15, 2026
@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto master (b4876bf), cleaned the commit message, and realigned the backslash continuations the parentheses had knocked out of column.

I checked every call site for an observable behavior change. There is none — this is prophylactic. Evidence below.

The PR title says "Parenthesise"; the commit subject is now BUG: Parenthesize macro arguments and replacement lists (US English). Worth retitling the PR to match.

The call-site check

An unparenthesized argument only misbehaves when a caller passes something whose precedence differs from a primary expression. Extracted every argument of every NT_FILL / NT_MAT* invocation in the tree:

$ grep -rhoE "NT_(FILL|MAT[0-9A-Z_]*)\([^)]*\)" nifti2/*.c niftilib/*.c \
    | sed 's/^[A-Z_0-9]*(//;s/)$//' | tr ',' '\n' | sed 's/ //g' | sort -u \
    | grep -vE '^[A-Za-z_][A-Za-z0-9_]*$'
1 3 4 5 8 10 15 16 18 24 80
nim->sto_xyz

Everything else is a plain identifier. The only non-identifier is nim->sto_xyz, and -> and . are both postfix and left-associative, so nim->sto_xyz.m[0][0] and (nim->sto_xyz).m[0][0] parse identically.

FSL_RADIOLOGICAL has two uses, int order=FSL_RADIOLOGICAL; and order=FSL_RADIOLOGICAL;. -1 and (-1) are the same there.

So there is no red to prove. Class is BUILD: 157 clang-tidy bugprone-macro-parentheses findings cleared, no runtime seam.

Installed-header check, re-run

Verified with clang -dM -E over each header, master vs this branch:

header installed? definitions that differ
fsliolib/fslio.h yes 1 — FSL_RADIOLOGICAL, -1 to (-1), same value
nifti2/nifti_tool.h no 8
niftilib/nifti1_tool.h no 1

The two tool headers are not in any target's PUBLIC_HEADER property, so they are not part of the installed interface. fslio.h is, and its single changed definition keeps its value.

Backslash continuations realigned

Inserting the parentheses pushed the line text past the continuation backslashes, leaving every touched macro block ragged. Each changed line now carries its backslash back at the column it had on master, where the text still fits. Only lines this commit already changes were touched: git diff master HEAD and git diff -w master HEAD both report 59 added lines, so there is no whitespace-only churn.

Message cleanup

Removed the Co-Authored-By: trailer naming an AI tool and the Claude-Session: URL; "Parenthesise"/"parenthesised"/"unparenthesised" to US spelling, including in the in-source NOLINT comment.

Replaced the bare #22 / #24 cross-references with a description in words — this is a fork, so a bare number resolves to a different PR upstream.

Corrected one claim: the body said NT_MAT33_TO_MAT44 "breaks for any argument that is not a plain identifier". It breaks for any argument that is not a postfix expression; nim->sto_xyz is not a plain identifier and works fine. The body now also states outright that no current call site is affected, rather than leaving that implied.

Build and test
  • Release, NIFTI_BUILD_APPLICATIONS=ON USE_NIFTI2_CODE=ON USE_CIFTI_CODE=ON USE_FSL_CODE=ON: 100% tests passed, 0 tests failed out of 362.
  • Same plus -DFSLSTYLE=ON (this PR touches fslio.h): 100% tests passed, 0 tests failed out of 364.

@hjmjohnson hjmjohnson changed the title BUG: Parenthesise macro arguments and replacement lists BUG: Parenthesize macro arguments and replacement lists Sep 22, 2026
@hjmjohnson
hjmjohnson force-pushed the pr/fix-macro-parentheses branch from 5952e1b to 857e6cb Compare September 22, 2026 15:54
Clears all 155 bugprone-macro-parentheses findings: 152 in
nifti2/nifti_tool.c, 2 in niftilib/nifti1_tool.c, 1 in fsliolib/fslio.c.

An unparenthesized argument takes the precedence of whatever the caller
passed. NT_FILL(..., rv) expands to `rv = ...` and NT_MAT33_TO_MAT44 to
`m44.m[0][0] = ...`, which break for an argument that is not already a
postfix expression. Every current caller passes a plain identifier, a
literal or `nim->sto_xyz`, so nothing observable moves.

The NT_DCONVERT dtype and stype parameters stay bare behind a NOLINT
fence: they name types, so `(dtype) * pd = dptr;` would be a cast
expression rather than a declaration. FSL_RADIOLOGICAL is in an installed
header, so its replacement list changes from -1 to (-1); its value does not.
@hjmjohnson
hjmjohnson force-pushed the pr/fix-macro-parentheses branch from 857e6cb to fffc091 Compare September 23, 2026 00:09
@hjmjohnson

Copy link
Copy Markdown
Member

Rebased onto current master (one conflict, resolved) and pushed as fffc091.
Measured red-green with the tool this PR exists to satisfy: 155
bugprone-macro-parentheses findings before, 0 after.

The conflict, and why the resolution takes both sides

master had already parenthesized nvals in NT_DCONVERT_W_CHECKS while leaving
failure bare; this PR did the opposite. Neither side was complete, so the
resolution keeps both:

      (failure) = 0;                            \
      for(index=0; index<(nvals); index++) {    \
Measured with clang-tidy, per file

.clang-tidy enables bugprone-macro-parentheses via bugprone-*, so the check
is the instrument here. Counts taken by swapping only the three headers between
master and this branch, against the same compile_commands.json:

Translation unit master this PR
nifti2/nifti_tool.c 152 0
niftilib/nifti1_tool.c 2 0
fsliolib/fslio.c 1 0
total 155 0

This corrects the numbers in the original commit message, which said 170 findings
with 157 cleared and 13 deferred to other PRs. Those other PRs have since merged,
so this now clears the whole set. The message is updated to match.

Two things fixed on top of the rebase

Backslash alignment had regressed. Parenthesizing lengthened the
two-assignment lines, which pushed their continuations past their neighbours.
master had each of these macros at a single backslash column:

Macro master before this fix now
NT_MAT33_TO_MAT44 58 58 and 66 65
NT_MAT44_TO_MAT33 58 58 and 66 65
NT_MAT44_SET_TO_IDENTITY 73 73 and 81 79

Each is realigned one space past its own longest line, so the whole block shifts
together rather than splitting into two columns.

dptr and sptr are deliberately left bare. They sit inside the NOLINT
fence, and I tried parenthesizing them before reverting it: a macro argument
cannot contain a top-level comma, so dtype * pd = (dptr); and
dtype * pd = dptr; are identical in meaning for every possible argument. The
parens bought nothing and cost 16 lines of realignment whitespace, which is the
wrong trade for a diff that should be about code.

Verification
Check Result
Release, cifti + FSL + FSLSTYLE=ON, applications on 370/370
Compiler diagnostics 0
bugprone-macro-parentheses, tree-wide (19 translation units) 0
Whitespace-only added lines 0

Churn is 63 added / 59 deleted; git diff -w reports 59 added, so 4 lines differ
only in the realignment.

Worth a follow-up, out of scope here: clang-tidy is configured but not run in CI,
so nothing mechanically prevents these 155 findings from coming back. A
run-clang-tidy leg would be the guard.

@hjmjohnson
hjmjohnson merged commit dbfb59d into InsightSoftwareConsortium:master Sep 23, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants