From 8bfc67564d3f27842a1e35318f4daa8a0976f8b9 Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Thu, 6 Aug 2026 14:10:42 -0400 Subject: [PATCH] fix: avoid collisions in duplicate-name suffixes Overloads get a `_N` suffix, but the suffixed name could match a real symbol with that name in the input. The generated header then declared the same identifier twice and did not compile. Skip suffixes that a real symbol or an earlier generated name already uses. Assisted-by: ClaudeCode:claude-fable-5 --- pybind11_mkdoc/mkdoc_lib.py | 7 ++++++- tests/duplicate_name_docs/duplicate_name.h | 16 ++++++++++++++++ tests/duplicate_name_test.py | 20 ++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/duplicate_name_docs/duplicate_name.h create mode 100644 tests/duplicate_name_test.py diff --git a/pybind11_mkdoc/mkdoc_lib.py b/pybind11_mkdoc/mkdoc_lib.py index 0a553d7..0a1b8e8 100755 --- a/pybind11_mkdoc/mkdoc_lib.py +++ b/pybind11_mkdoc/mkdoc_lib.py @@ -717,12 +717,17 @@ def write_header(comments, out_file=sys.stdout): file=out_file, ) + # A suffixed name must not collide with a real symbol of that name, or with an earlier suffixed name. + taken = {name for name, _, _ in comments} name_ctr = 1 name_prev = None for name, _, comment in sorted(comments, key=lambda x: (x[0], x[1])): if name == name_prev: name_ctr += 1 - name = name + f"_{name_ctr}" + while f"{name_prev}_{name_ctr}" in taken: + name_ctr += 1 + name = f"{name_prev}_{name_ctr}" + taken.add(name) else: name_prev = name name_ctr = 1 diff --git a/tests/duplicate_name_docs/duplicate_name.h b/tests/duplicate_name_docs/duplicate_name.h new file mode 100644 index 0000000..d3029fe --- /dev/null +++ b/tests/duplicate_name_docs/duplicate_name.h @@ -0,0 +1,16 @@ +#pragma once + +/// First overload of foo. +void foo(int x); + +/// Second overload of foo. +void foo(double x); + +/// Third overload of foo. +void foo(char x); + +/// A real symbol that clashes with the suffix of the second foo overload. +void foo_2(); + +/// A real symbol that clashes with the suffix of the third foo overload. +void foo_3(); diff --git a/tests/duplicate_name_test.py b/tests/duplicate_name_test.py new file mode 100644 index 0000000..a42969e --- /dev/null +++ b/tests/duplicate_name_test.py @@ -0,0 +1,20 @@ +import re +from pathlib import Path + +import pybind11_mkdoc + +DIR = Path(__file__).resolve().parent + +NAME_RE = re.compile(r"^static const char \*(\w+) =", re.MULTILINE) + + +def test_suffixed_names_do_not_collide(tmp_path): + comments = pybind11_mkdoc.mkdoc_lib.extract_all([str(DIR / "duplicate_name_docs" / "duplicate_name.h")]) + + output = tmp_path / "docs.h" + with output.open("w") as fd: + pybind11_mkdoc.mkdoc_lib.write_header(comments, fd) + + names = NAME_RE.findall(output.read_text()) + assert len(names) == len(comments) + assert len(names) == len(set(names))