Skip to content

Commit 05cc39d

Browse files
authored
[rst] Improve unreferenced footnote warnings (#12730)
The previous `UnreferencedFootnotesDetector` transform was untested and missed warnings for a number of cases. This commit adds a test, to cover a reasonable range of scenarios, then changes how the detection works to pass this test. The transform now runs just after the docutils `Footnote` resolution transform (changing its priority from 200 to 622) then simply check for any footnotes without "back-references".
1 parent df871ab commit 05cc39d

File tree

3 files changed

+76
-13
lines changed

3 files changed

+76
-13
lines changed

CHANGES.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,11 @@ Bugs fixed
2020
:confval:`intersphinx_cache_limit`.
2121
Patch by Shengyu Zhang.
2222

23+
* #12730: The ``UnreferencedFootnotesDetector`` transform has been improved
24+
to more consistently detect unreferenced footnotes.
25+
Note, the priority of the transform has been changed from 200 to 622,
26+
so that it now runs after the docutils ``Footnotes`` resolution transform.
27+
Patch by Chris Sewell.
28+
2329
Testing
2430
-------

sphinx/transforms/__init__.py

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from docutils import nodes
1010
from docutils.transforms import Transform, Transformer
1111
from docutils.transforms.parts import ContentsFilter
12+
from docutils.transforms.references import Footnotes
1213
from docutils.transforms.universal import SmartQuotes
1314
from docutils.utils import normalize_language_tag
1415
from docutils.utils.smartquotes import smartchars
@@ -294,23 +295,40 @@ class UnreferencedFootnotesDetector(SphinxTransform):
294295
Detect unreferenced footnotes and emit warnings
295296
"""
296297

297-
default_priority = 200
298+
default_priority = Footnotes.default_priority + 2
298299

299300
def apply(self, **kwargs: Any) -> None:
300301
for node in self.document.footnotes:
301-
if node['names'] == []:
302-
# footnote having duplicated number. It is already warned at parser.
303-
pass
304-
elif node['names'][0] not in self.document.footnote_refs:
305-
logger.warning(__('Footnote [%s] is not referenced.'), node['names'][0],
306-
type='ref', subtype='footnote',
307-
location=node)
308-
302+
# note we do not warn on duplicate footnotes here
303+
# (i.e. where the name has been moved to dupnames)
304+
# since this is already reported by docutils
305+
if not node['backrefs'] and node["names"]:
306+
logger.warning(
307+
__('Footnote [%s] is not referenced.'),
308+
node['names'][0] if node['names'] else node['dupnames'][0],
309+
type='ref',
310+
subtype='footnote',
311+
location=node
312+
)
313+
for node in self.document.symbol_footnotes:
314+
if not node['backrefs']:
315+
logger.warning(
316+
__('Footnote [*] is not referenced.'),
317+
type='ref',
318+
subtype='footnote',
319+
location=node
320+
)
309321
for node in self.document.autofootnotes:
310-
if not any(ref['auto'] == node['auto'] for ref in self.document.autofootnote_refs):
311-
logger.warning(__('Footnote [#] is not referenced.'),
312-
type='ref', subtype='footnote',
313-
location=node)
322+
# note we do not warn on duplicate footnotes here
323+
# (i.e. where the name has been moved to dupnames)
324+
# since this is already reported by docutils
325+
if not node['backrefs'] and node["names"]:
326+
logger.warning(
327+
__('Footnote [#] is not referenced.'),
328+
type='ref',
329+
subtype='footnote',
330+
location=node
331+
)
314332

315333

316334
class DoctestTransform(SphinxTransform):
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Test the ``UnreferencedFootnotesDetector`` transform."""
2+
3+
from pathlib import Path
4+
5+
from sphinx.testing.util import SphinxTestApp
6+
from sphinx.util.console import strip_colors
7+
8+
9+
def test_warnings(make_app: type[SphinxTestApp], tmp_path: Path) -> None:
10+
"""Test that warnings are emitted for unreferenced footnotes."""
11+
tmp_path.joinpath("conf.py").touch()
12+
tmp_path.joinpath("index.rst").write_text(
13+
"""
14+
Title
15+
=====
16+
[1]_ [#label2]_
17+
18+
.. [1] This is a normal footnote.
19+
.. [2] This is a normal footnote.
20+
.. [2] This is a normal footnote.
21+
.. [3] This is a normal footnote.
22+
.. [*] This is a symbol footnote.
23+
.. [#] This is an auto-numbered footnote.
24+
.. [#label1] This is an auto-numbered footnote with a label.
25+
.. [#label1] This is an auto-numbered footnote with a label.
26+
.. [#label2] This is an auto-numbered footnote with a label.
27+
""", encoding="utf8"
28+
)
29+
app = make_app(srcdir=tmp_path)
30+
app.build()
31+
warnings = strip_colors(app.warning.getvalue()).replace(str(tmp_path / "index.rst"), "source/index.rst")
32+
print(warnings)
33+
assert warnings.strip() == """
34+
source/index.rst:8: WARNING: Duplicate explicit target name: "2". [docutils]
35+
source/index.rst:13: WARNING: Duplicate explicit target name: "label1". [docutils]
36+
source/index.rst:9: WARNING: Footnote [3] is not referenced. [ref.footnote]
37+
source/index.rst:10: WARNING: Footnote [*] is not referenced. [ref.footnote]
38+
source/index.rst:11: WARNING: Footnote [#] is not referenced. [ref.footnote]
39+
""".strip()

0 commit comments

Comments
 (0)