@@ -1700,26 +1700,78 @@ def __init__(
17001700 self .definition_pattern = definition_pattern
17011701 self .usage_pattern = usage_pattern
17021702 self .defined : Set [str ] = set ()
1703- self .used : Dict [str , List [Tuple [str , int ]]] = defaultdict (list )
1703+ self .used : Dict [str , List [Tuple [str , int , int , int ]]] = defaultdict (list )
17041704
17051705 def check_line (self , line_num : int , line : str ) -> None :
17061706 for m in self .definition_pattern .finditer (line ):
17071707 self .defined .add (m .group (1 ))
17081708 for pattern in self .usage_pattern :
17091709 for m in pattern .finditer (line ):
1710- name = m .group (1 )
1711- self .used [name ].append ((os .path .relpath (self .file_path ), line_num ))
1710+ for name in m .group (1 ).split ("," ):
1711+ name = name .strip ()
1712+ if not name or name .startswith ("\\ " ):
1713+ continue
1714+ self .used [name ].append (
1715+ (
1716+ os .path .relpath (self .file_path ),
1717+ line_num ,
1718+ m .start (1 ),
1719+ m .end (1 ),
1720+ )
1721+ )
17121722
17131723 def end_checks (self ) -> None :
17141724 for name , locations in sorted (self .used .items ()):
17151725 if name not in self .defined :
1716- file_name , line_num = locations [0 ]
1726+ file_name , line_num , column_start , column_end = locations [0 ]
17171727 emit_check_failure (
17181728 self ,
17191729 file_name ,
17201730 line_num ,
1721- 0 ,
1722- 0 ,
1731+ column_start ,
1732+ column_end ,
1733+ f"`{ name } ` has no definition." ,
1734+ )
1735+
1736+
1737+ class RefUndefinedCheck (Check ):
1738+ """Checks whether any uses of `\\ ref` or `\\ iref` are dangling."""
1739+
1740+ SECTION_PATTERN = re .compile (r"\\rSec[0-9]\[([^\]]+)\]" )
1741+ DEFINITION_PATTERN = re .compile (r"\\definition\{[^\}]*?\}\{(.+?)\}" )
1742+ ETC_PATTERN = re .compile (r"\\(?:infannex|normannex|label)\{(.+?)\}" )
1743+ DEFINING_PATTERNS = [SECTION_PATTERN , DEFINITION_PATTERN , ETC_PATTERN ]
1744+
1745+ # TODO: Excluding `:` means that `tab:...` references are currently unchecked.
1746+ # Maybe that could be added in the future,
1747+ # but there are lots of different kinds of tables that could be defining.
1748+ REF_IREF_PATTERN = re .compile (r"\\i?ref\{([^}\\:]+)\}" )
1749+
1750+ def __init__ (self , check_id : str ):
1751+ self .id = check_id
1752+ self .defined : Set [str ] = set ()
1753+ self .used : Dict [str , List [Tuple [str , int , int , int ]]] = defaultdict (list )
1754+
1755+ def check_line (self , line_num : int , line : str ) -> None :
1756+ for defining_pattern in self .DEFINING_PATTERNS :
1757+ for m in defining_pattern .finditer (line ):
1758+ self .defined .add (m .group (1 ))
1759+ for m in self .REF_IREF_PATTERN .finditer (line ):
1760+ for usage in m .group (1 ).split ("," ):
1761+ self .used [usage .strip ()].append (
1762+ (os .path .relpath (self .file_path ), line_num , m .start (1 ), m .end (1 ))
1763+ )
1764+
1765+ def end_checks (self ) -> None :
1766+ for name , locations in sorted (self .used .items ()):
1767+ if name not in self .defined :
1768+ file_name , line_num , column_start , column_end = locations [0 ]
1769+ emit_check_failure (
1770+ self ,
1771+ file_name ,
1772+ line_num ,
1773+ column_start ,
1774+ column_end ,
17231775 f"`{ name } ` has no definition." ,
17241776 )
17251777
@@ -1895,6 +1947,7 @@ def end_checks(self) -> None:
18951947 SubclausesWithoutSiblingsCheck ("base-lonely-subclause" ),
18961948 UnknownCommandCheck ("base-unknown-command" ),
18971949 SectionSelfReferenceCheck ("base-self-ref" ),
1950+ RefUndefinedCheck ("base-ref-undef" ),
18981951 UseOfUndefinedCheck (
18991952 "base-grammarterm-undef" ,
19001953 definition_pattern = re .compile (r"\\nontermdef\{([^}]+)\}" ),
0 commit comments