[llvm] [Github] Make code-format-helper --extensions ordering deterministic (PR #221677)
Anton Dziatkovskii via llvm-commits
llvm-commits at lists.llvm.org
Mon Sep 7 01:34:58 PDT 2026
https://github.com/tonydzi created https://github.com/llvm/llvm-project/pull/221677
`ClangFormatHelper._construct_command` collects the extensions of the changed files into a `set` and joins it directly:
```python
extensions = set()
for file in self._cpp_files:
_, ext = os.path.splitext(file)
extensions.add(ext.strip("."))
cf_cmd.append("--extensions")
cf_cmd.append(",".join(extensions))
```
Python randomizes `str` hashing per process, so the `--extensions` argument comes out in a different order on every run for the *same* set of changed files.
### Why it is visible
That command string is not internal. It is the one embedded in the bot's PR comment under *"You can test this locally with the following command"* (via the `instructions` property), and it is what gets printed to the CI log by `print(f"Running: {' '.join(cf_cmd)}")`. So identical input produces a different reproducer line on each run, and re-running the formatting job on an unchanged PR rewrites the comment with a command that differs only in extension order.
### Reproducer
```python
import os, subprocess, sys
FILES = ["clang/a.cpp", "clang/b.h", "clang/c.inc",
"clang/d.cl", "clang/e.cxx", "clang/f.hpp"]
SNIPPET = r'''
import os, sys
extensions = set()
for file in sys.argv[1:]:
_, ext = os.path.splitext(file)
extensions.add(ext.strip("."))
print(",".join(extensions))
'''
seen = set()
for seed in range(1, 13):
env = dict(os.environ, PYTHONHASHSEED=str(seed))
seen.add(subprocess.run([sys.executable, "-c", SNIPPET, *FILES],
capture_output=True, text=True, env=env).stdout.strip())
print(f"distinct --extensions strings across 12 runs: {len(seen)}")
for s in sorted(seen):
print(" ", s)
assert len(seen) == 1, f"NON-DETERMINISTIC: {len(seen)} different strings for identical input"
```
Before the change — 12 runs, 12 different arguments, assertion fails (exit 1):
```
distinct --extensions strings across 12 runs: 12
cl,cxx,cpp,h,hpp,inc
cl,h,cxx,hpp,inc,cpp
cpp,cxx,h,hpp,inc,cl
cpp,hpp,h,inc,cxx,cl
cxx,h,cl,inc,hpp,cpp
h,cl,inc,cpp,hpp,cxx
h,cpp,inc,cxx,cl,hpp
h,inc,cl,cpp,cxx,hpp
hpp,cl,h,cpp,inc,cxx
hpp,inc,cl,cpp,cxx,h
inc,cl,h,cxx,hpp,cpp
inc,hpp,cpp,cxx,h,cl
AssertionError: NON-DETERMINISTIC: 12 different strings for identical input
```
After the change (same script with `sorted(extensions)`) — 1 distinct string, assertion passes (exit 0):
```
distinct --extensions strings across 12 runs: 1
cl,cpp,cxx,h,hpp,inc
```
### Note on extensionless files
`should_include_extensionless_file` intentionally lets `libcxx/include` headers through, which puts an empty string into the set. That is load-bearing: `git-clang-format`'s `filter_by_extension` keeps extensionless files only when `"" in allowed_extensions`. Sorting preserves the entry (it simply sorts first), so `libcxx/include` headers are unaffected — verified with `["libcxx/include/vector", "clang/a.cpp"]`, which still yields `,cpp`.
I have read and reviewed this change and can answer questions about it. Tool assistance is disclosed with an `Assisted-by:` trailer in the commit message, per the [AI tool policy](https://llvm.org/docs/AIToolPolicy.html).
>From 4b133f5f56826cc4b86d4156f21662d92f60a594 Mon Sep 17 00:00:00 2001
From: Anton Dziatkovskii <bbplatinumdepartment at gmail.com>
Date: Mon, 7 Sep 2026 01:10:45 -0700
Subject: [PATCH] [Github] Make code-format-helper --extensions ordering
deterministic
`ClangFormatHelper._construct_command` gathers the extensions of the changed
files into a `set` and joins it directly, so the `--extensions` argument is
emitted in an arbitrary order that changes between runs (str hashing is
randomized per process).
The same command string is embedded in the "You can test this locally with the
following command" block of the formatting bot's PR comment and printed to the
CI log, so identical input produced a different reproducer line on every run.
Sorting the set makes the output stable. The empty-string entry that
`should_include_extensionless_file` relies on for `libcxx/include` headers is
preserved -- git-clang-format's `filter_by_extension` explicitly tests for
`"" in allowed_extensions`.
Assisted-by: Claude (Claude Code)
---
llvm/utils/git/code-format-helper.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/llvm/utils/git/code-format-helper.py b/llvm/utils/git/code-format-helper.py
index 1e8c332554a8e..58128dba899a0 100755
--- a/llvm/utils/git/code-format-helper.py
+++ b/llvm/utils/git/code-format-helper.py
@@ -197,7 +197,7 @@ def _construct_command(self, diff_expression: list[str] | None):
ext.strip(".")
) # Exclude periods since git-clang-format takes extensions without them
cf_cmd.append("--extensions")
- cf_cmd.append(",".join(extensions))
+ cf_cmd.append(",".join(sorted(extensions)))
cf_cmd.append("--")
cf_cmd += self._cpp_files
More information about the llvm-commits
mailing list