Repository: sphinx-doc/sphinx

Issue:
LaTeX: reST label before a ``module`` (or ``automodule``) sphinx.ext.autodoc directive causes pdf build to report multiply defined reference
### Describe the bug

As said in the title, the PDF build reports multiply-defined references.

### How to Reproduce

Use 
```
import os
import sys
sys.path.insert(0, os.path.abspath('.'))

extensions = ['sphinx.ext.autodoc']
```

in `conf.py`

file `index.rst`:
```rest
Welcome to FOO's documentation!
===============================

.. toctree::

   testfile
```

file `testfile.rst`:
```rest
.. _somelabel:

.. module:: testmodule

Some title
==========

Some text.

.. autofunction:: foo
```

file `testmodule.py`
```python
"""not much here
"""

def foo():
    "I do not do much and simply return ``None``"

    return None

```
(the  autofunction part is not part of the problem).

Then run `make latexpdf`.

Console output ends with
```
Latexmk: ====List of undefined refs and citations:
  Label `testfile:somelabel' multiply defined
```

This is not a serious problem (meaning it does not cause build failure).

But it happens in real life, see https://github.com/numpy/numpy/issues/22930#issuecomment-1371989684 and the 
```
LaTeX Warning: Label `reference/maskedarray.generic:maskedarray-generic' multip
ly defined.
```
is an instance of the present issue.  I have not yet checked but expect the others reported there as well.

### Environment Information

```text
Platform:              darwin
Python version:        3.10.9 (v3.10.9:1dd9be6584, Dec  6 2022, 14:37:36) [Clang 13.0.0 (clang-1300.0.29.30)])
Python implementation: CPython
Sphinx version:        6.2.0+/6e6b9ad45
Docutils version:      0.19
Jinja2 version:        3.1.2
Pygments version:      2.14.0
```


### Sphinx extensions

```python
``["sphinx.ext.autodoc"]``
```

Edit: all NumPy instances from building their `numpy-ref.pdf` are of this nature: https://github.com/numpy/numpy/issues/22930#issuecomment-1372520387


Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index e70998055..da414f1e2 100644
--- a/CHANGES
+++ b/CHANGES
@@ -75,6 +75,9 @@ Bugs fixed
 
 * #11079: LaTeX: figures with align attribute may disappear and strangely impact
   following lists
+* #11093: LaTeX: fix "multiply-defined references" PDF build warnings when one or
+  more reST labels directly precede an :rst:dir:`py:module` or :rst:dir:`automodule`
+  directive. Patch by Bénédikt Tran (picnixz)
 * #11110: LaTeX: Figures go missing from latex pdf if their files have the same
   base name and they use a post transform.  Patch by aaron-cooper
 * LaTeX: fix potential color leak from shadow to border of rounded boxes, if
diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py
index 215fd4c76..efb847353 100644
--- a/sphinx/writers/latex.py
+++ b/sphinx/writers/latex.py
@@ -1499,7 +1499,26 @@ def add_target(id: str) -> None:
                 pass
             else:
                 add_target(node['refid'])
-        for id in node['ids']:
+        # Temporary fix for https://github.com/sphinx-doc/sphinx/issues/11093
+        # TODO: investigate if a more elegant solution exists (see comments of #11093)
+        if node.get('ismod', False):
+            # Detect if the previous nodes are label targets. If so, remove
+            # the refid thereof from node['ids'] to avoid duplicated ids.
+            def has_dup_label(sib: Node | None) -> bool:
+                return isinstance(sib, nodes.target) and sib.get('refid') in node['ids']
+
+            prev = get_prev_node(node)
+            if has_dup_label(prev):
+                ids = node['ids'][:]  # copy to avoid side-effects
+                while has_dup_label(prev):
+                    ids.remove(prev['refid'])  # type: ignore
+                    prev = get_prev_node(prev)
+            else:
+                ids = iter(node['ids'])  # read-only iterator
+        else:
+            ids = iter(node['ids'])  # read-only iterator
+
+        for id in ids:
             add_target(id)
 
     def depart_target(self, node: Element) -> None:


Test patch:
diff --git a/tests/roots/test-latex-labels-before-module/automodule1.py b/tests/roots/test-latex-labels-before-module/automodule1.py
new file mode 100644
index 000000000..0545aa427
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/automodule1.py
@@ -0,0 +1,2 @@
+"""docstring"""
+
diff --git a/tests/roots/test-latex-labels-before-module/automodule2a.py b/tests/roots/test-latex-labels-before-module/automodule2a.py
new file mode 100644
index 000000000..0545aa427
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/automodule2a.py
@@ -0,0 +1,2 @@
+"""docstring"""
+
diff --git a/tests/roots/test-latex-labels-before-module/automodule2b.py b/tests/roots/test-latex-labels-before-module/automodule2b.py
new file mode 100644
index 000000000..0545aa427
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/automodule2b.py
@@ -0,0 +1,2 @@
+"""docstring"""
+
diff --git a/tests/roots/test-latex-labels-before-module/automodule3.py b/tests/roots/test-latex-labels-before-module/automodule3.py
new file mode 100644
index 000000000..0545aa427
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/automodule3.py
@@ -0,0 +1,2 @@
+"""docstring"""
+
diff --git a/tests/roots/test-latex-labels-before-module/conf.py b/tests/roots/test-latex-labels-before-module/conf.py
new file mode 100644
index 000000000..25193b0dd
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/conf.py
@@ -0,0 +1,8 @@
+import os
+import sys
+
+sys.path.insert(0, os.path.abspath('.'))
+
+extensions = ['sphinx.ext.autodoc']
+
+nitpicky = True
diff --git a/tests/roots/test-latex-labels-before-module/index.rst b/tests/roots/test-latex-labels-before-module/index.rst
new file mode 100644
index 000000000..e6df74922
--- /dev/null
+++ b/tests/roots/test-latex-labels-before-module/index.rst
@@ -0,0 +1,48 @@
+latex-labels-before-module
+==========================
+
+.. _label_1a:
+.. _label_1b:
+
+.. module:: module1
+
+   text
+
+.. _label_2:
+
+.. module:: module2a
+
+   text
+
+.. module:: module2b
+
+   text
+
+.. _label_3:
+
+.. module:: module3
+
+   text
+
+.. _label_auto_1a:
+.. _label_auto_1b:
+
+.. automodule:: automodule1
+
+   text
+
+.. _label_auto_2:
+
+.. automodule:: automodule2a
+
+   text
+
+.. automodule:: automodule2b
+
+   text
+
+.. _label_auto_3:
+
+.. automodule:: automodule3
+
+   text
diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py
index 7fb59dbc9..faf1a47b7 100644
--- a/tests/test_build_latex.py
+++ b/tests/test_build_latex.py
@@ -1708,3 +1708,33 @@ def test_copy_images(app, status, warning):
         'rimg.png',
         'testimäge.png',
     }
+
+
+@pytest.mark.sphinx('latex', testroot='latex-labels-before-module')
+def test_duplicated_labels_before_module(app, status, warning):
+    app.build()
+    content: str = (app.outdir / 'python.tex').read_text()
+
+    def count_label(name):
+        text = r'\phantomsection\label{\detokenize{%s}}' % name
+        return content.count(text)
+
+    pattern = r'\\phantomsection\\label\{\\detokenize\{index:label-(?:auto-)?\d+[a-z]*}}'
+    # labels found in the TeX output
+    output_labels = frozenset(match.group() for match in re.finditer(pattern, content))
+    # labels that have been tested and occurring exactly once in the output
+    tested_labels = set()
+
+    # iterate over the (explicit) labels in the corresponding index.rst
+    for rst_label_name in [
+        'label_1a', 'label_1b', 'label_2', 'label_3',
+        'label_auto_1a', 'label_auto_1b', 'label_auto_2', 'label_auto_3',
+    ]:
+        tex_label_name = 'index:' + rst_label_name.replace('_', '-')
+        tex_label_code = r'\phantomsection\label{\detokenize{%s}}' % tex_label_name
+        assert content.count(tex_label_code) == 1, f'duplicated label: {tex_label_name!r}'
+        tested_labels.add(tex_label_code)
+
+    # ensure that we did not forget any label to check
+    # and if so, report them nicely in case of failure
+    assert sorted(tested_labels) == sorted(output_labels)