Skip to content

Commit

Permalink
[rst] Improve unreferenced footnote warnings (#12730)
Browse files Browse the repository at this point in the history
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".
  • Loading branch information
chrisjsewell authored Aug 5, 2024
1 parent df871ab commit 05cc39d
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 13 deletions.
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@ Bugs fixed
:confval:`intersphinx_cache_limit`.
Patch by Shengyu Zhang.

* #12730: The ``UnreferencedFootnotesDetector`` transform has been improved
to more consistently detect unreferenced footnotes.
Note, the priority of the transform has been changed from 200 to 622,
so that it now runs after the docutils ``Footnotes`` resolution transform.
Patch by Chris Sewell.

Testing
-------
44 changes: 31 additions & 13 deletions sphinx/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from docutils import nodes
from docutils.transforms import Transform, Transformer
from docutils.transforms.parts import ContentsFilter
from docutils.transforms.references import Footnotes
from docutils.transforms.universal import SmartQuotes
from docutils.utils import normalize_language_tag
from docutils.utils.smartquotes import smartchars
Expand Down Expand Up @@ -294,23 +295,40 @@ class UnreferencedFootnotesDetector(SphinxTransform):
Detect unreferenced footnotes and emit warnings
"""

default_priority = 200
default_priority = Footnotes.default_priority + 2

def apply(self, **kwargs: Any) -> None:
for node in self.document.footnotes:
if node['names'] == []:
# footnote having duplicated number. It is already warned at parser.
pass
elif node['names'][0] not in self.document.footnote_refs:
logger.warning(__('Footnote [%s] is not referenced.'), node['names'][0],
type='ref', subtype='footnote',
location=node)

# note we do not warn on duplicate footnotes here
# (i.e. where the name has been moved to dupnames)
# since this is already reported by docutils
if not node['backrefs'] and node["names"]:
logger.warning(
__('Footnote [%s] is not referenced.'),
node['names'][0] if node['names'] else node['dupnames'][0],
type='ref',
subtype='footnote',
location=node
)
for node in self.document.symbol_footnotes:
if not node['backrefs']:
logger.warning(
__('Footnote [*] is not referenced.'),
type='ref',
subtype='footnote',
location=node
)
for node in self.document.autofootnotes:
if not any(ref['auto'] == node['auto'] for ref in self.document.autofootnote_refs):
logger.warning(__('Footnote [#] is not referenced.'),
type='ref', subtype='footnote',
location=node)
# note we do not warn on duplicate footnotes here
# (i.e. where the name has been moved to dupnames)
# since this is already reported by docutils
if not node['backrefs'] and node["names"]:
logger.warning(
__('Footnote [#] is not referenced.'),
type='ref',
subtype='footnote',
location=node
)


class DoctestTransform(SphinxTransform):
Expand Down
39 changes: 39 additions & 0 deletions tests/test_transforms/test_unreferenced_footnotes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Test the ``UnreferencedFootnotesDetector`` transform."""

from pathlib import Path

from sphinx.testing.util import SphinxTestApp
from sphinx.util.console import strip_colors


def test_warnings(make_app: type[SphinxTestApp], tmp_path: Path) -> None:
"""Test that warnings are emitted for unreferenced footnotes."""
tmp_path.joinpath("conf.py").touch()
tmp_path.joinpath("index.rst").write_text(
"""
Title
=====
[1]_ [#label2]_
.. [1] This is a normal footnote.
.. [2] This is a normal footnote.
.. [2] This is a normal footnote.
.. [3] This is a normal footnote.
.. [*] This is a symbol footnote.
.. [#] This is an auto-numbered footnote.
.. [#label1] This is an auto-numbered footnote with a label.
.. [#label1] This is an auto-numbered footnote with a label.
.. [#label2] This is an auto-numbered footnote with a label.
""", encoding="utf8"
)
app = make_app(srcdir=tmp_path)
app.build()
warnings = strip_colors(app.warning.getvalue()).replace(str(tmp_path / "index.rst"), "source/index.rst")
print(warnings)
assert warnings.strip() == """
source/index.rst:8: WARNING: Duplicate explicit target name: "2". [docutils]
source/index.rst:13: WARNING: Duplicate explicit target name: "label1". [docutils]
source/index.rst:9: WARNING: Footnote [3] is not referenced. [ref.footnote]
source/index.rst:10: WARNING: Footnote [*] is not referenced. [ref.footnote]
source/index.rst:11: WARNING: Footnote [#] is not referenced. [ref.footnote]
""".strip()

0 comments on commit 05cc39d

Please sign in to comment.