From 05cc39d9b2a64b09504c06bfc2299aad96c85ccd Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Mon, 5 Aug 2024 10:28:13 +0200 Subject: [PATCH] [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". --- CHANGES.rst | 6 +++ sphinx/transforms/__init__.py | 44 +++++++++++++------ .../test_unreferenced_footnotes.py | 39 ++++++++++++++++ 3 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 tests/test_transforms/test_unreferenced_footnotes.py diff --git a/CHANGES.rst b/CHANGES.rst index 1d04bc27010..4b920ca3911 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------- diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 7ccef6aacaa..5fe133137fd 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -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 @@ -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): diff --git a/tests/test_transforms/test_unreferenced_footnotes.py b/tests/test_transforms/test_unreferenced_footnotes.py new file mode 100644 index 00000000000..938adcb7fc6 --- /dev/null +++ b/tests/test_transforms/test_unreferenced_footnotes.py @@ -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()