Skip to content

Commit

Permalink
Warn if anchor superseding other matches
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisjsewell committed Jan 5, 2023
1 parent acfe50d commit 809144d
Show file tree
Hide file tree
Showing 6 changed files with 96 additions and 54 deletions.
14 changes: 13 additions & 1 deletion docs/syntax/optional.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,20 @@ Anchors in other files should be relative to the current file, for example
`[**link text**](syntax.md#core-syntax)`: [**link text**](syntax.md#core-syntax).

:::{important}
When resolving references, heading anchors will take precedence over any other targets with the same name.
When resolving references, heading anchors will take precedence over any other targets with the same name, and a warning will be emitted, such as:

```
WARNING: 'a-title' anchor superseding other matches: 'std:label:a-title' [myst.xref_anchor]
```

To suppress these warnings, use the `suppress_warnings` configuration option:

```python
suppress_warnings = ["myst.xref_anchor"]
```
:::

:::{seealso}
For more details on referencing see: <project:#syntax/referencing>.
:::

Expand Down
2 changes: 1 addition & 1 deletion docs/syntax/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@ This is useful for long API names, where you may wish to specify the class, but
If a referenced target is present in multiple domains and/or object types, then you will see a warning such as:

```
<src>/test.md:2: WARNING: Multiple matches found for target '*:*:duplicate': 'std:label:duplicate','std:term:duplicate' [myst.xref_duplicate]
<src>/test.md:2: WARNING: Multiple matches found for target '*:*:duplicate': 'std:label:duplicate','std:term:duplicate' [myst.xref_ambiguous]
```

In this case, you can use the query string to filter matches by `domain:object_type`.
Expand Down
63 changes: 38 additions & 25 deletions myst_parser/mdit_to_docutils/local_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ def inventory_search_args(
return ref_target, ref_domain, ref_object_type, match_end


def truncated_join(delimiter: str, lst: list[str], max_items: int = 3) -> str:
"""Join a list of strings, truncating if it exceeds a maximum length."""
if len(lst) > max_items:
lst = lst[:max_items] + ["..."]
return delimiter.join(lst)


class MdDocumentLinks(Transform):
"""Replace markdown links [text](#ref "title").
Expand All @@ -87,23 +94,38 @@ def apply(self):
[ref_domain or "*", ref_object_type or "*", link_node.refname]
)

# look at anchors first
matches = resolve_myst_inventory(
# match auto-generated heading anchors
anchor_matches = resolve_myst_inventory(
{"myst": {"anchor": heading_anchors}}, # type: ignore
ref_target,
has_domain=ref_domain,
has_type=ref_object_type,
match_end=match_end,
)
if not matches:
# if no anchors, then look at std refs
matches = resolve_myst_inventory(
{"std": {"label": std_refs}}, # type: ignore
ref_target,
has_domain=ref_domain,
has_type=ref_object_type,
match_end=match_end,
)
matches = resolve_myst_inventory(
{"std": {"label": std_refs}}, # type: ignore
ref_target,
has_domain=ref_domain,
has_type=ref_object_type,
match_end=match_end,
)

# if there are anchor matches, then these take priority
# but warn if there are also other matches, so the user knows,
# and can choose to ignore
if anchor_matches:
if matches:
match_str = truncated_join(
",", [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches], 4
)
create_warning(
self.document,
f"{anchor_matches[0].name!r} anchor superseding other matches: "
f"{match_str}",
subtype=MystWarnings.XREF_ANCHOR,
line=link_node.line,
)
matches = anchor_matches

if not matches:
create_warning(
Expand All @@ -123,22 +145,13 @@ def apply(self):
continue

if len(matches) > 1:
# filter out matches to anchors
matches = [
m
for m in matches
if not (m.domain == "myst" and m.otype == "anchor")
]

if len(matches) > 1:
match_items = [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches]
if len(match_items) > 4:
match_items = match_items[:4] + ["..."]
match_str = truncated_join(
",", [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches], 4
)
create_warning(
self.document,
f"Multiple targets found for {loc_str!r}: "
f"{','.join(match_items)}",
subtype=MystWarnings.XREF_DUPLICATE,
f"Multiple targets found for {loc_str!r}: {match_str}",
subtype=MystWarnings.XREF_AMBIGUOUS,
line=link_node.line,
)

Expand Down
55 changes: 34 additions & 21 deletions myst_parser/sphinx_ext/references.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
LocalAnchorType,
gather_anchors,
inventory_search_args,
truncated_join,
)
from myst_parser.warnings import MystWarnings

Expand Down Expand Up @@ -269,26 +270,40 @@ def _resolve_xref_project(
[ref_domain or "*", ref_object_type or "*", node["reftarget"]]
)

# look at any auto-generated heading anchors for the target/local doc first
# these take priority over any other matches
# match auto-generated heading anchors for the target/local doc
myst_domain: MystDomain = self.env.get_domain("myst") # type: ignore
matches = myst_domain.get_anchor_matches(
anchor_matches = myst_domain.get_anchor_matches(
ref_domain,
ref_object_type,
ref_docname or node["refdoc"],
ref_target,
match_end,
)
if not matches:
# if none, get matches from the project inventory
matches = resolve_myst_inventory(
inventory,
ref_target,
has_domain=ref_domain,
has_type=ref_object_type,
has_docname=ref_docname,
match_end=match_end,
)
# match all other objects in the inventory
matches = resolve_myst_inventory(
inventory,
ref_target,
has_domain=ref_domain,
has_type=ref_object_type,
has_docname=ref_docname,
match_end=match_end,
)

# if there are anchor matches, then these take priority
# but warn if there are also other matches, so the user knows,
# and can choose to ignore
if anchor_matches:
if matches:
match_str = truncated_join(
",", [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches], 4
)
log_warning(
f"{anchor_matches[0].name!r} anchor superseding other matches: "
f"{match_str}",
location=node,
subtype=MystWarnings.XREF_ANCHOR,
)
matches = anchor_matches

# handle none or multiple matches
doc_str = f" in doc {ref_docname!r}" if ref_docname else ""
Expand All @@ -300,17 +315,15 @@ def _resolve_xref_project(
)
return None
if len(matches) > 1:
match_items = [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches]
if len(match_items) > 4:
match_items = match_items[:4] + ["..."]
match_str = truncated_join(
",", [f"'{r.domain}:{r.otype}:{r.name}'" for r in matches], 4
)
log_warning(
f"Multiple targets found for {loc_str!r}{doc_str}: "
f"{','.join(match_items)}",
subtype=MystWarnings.XREF_DUPLICATE,
f"Multiple targets found for {loc_str!r}{doc_str}: {match_str}",
subtype=MystWarnings.XREF_AMBIGUOUS,
location=node,
)

# TODO sort multiple matches by priority (e.g. local first, std domain)
target = matches[0]

ref_node = nodes.reference(
Expand Down Expand Up @@ -551,7 +564,7 @@ def _resolve_xref_inventory(
matches = matches[:4] + ["..."]
log_warning(
f"Multiple targets found for {loc!r}: {','.join(matches)}",
subtype=MystWarnings.IREF_DUPLICATE,
subtype=MystWarnings.IREF_AMBIGUOUS,
location=node,
)
return None
Expand Down
8 changes: 5 additions & 3 deletions myst_parser/warnings.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,21 @@ class MystWarnings(Enum):
"""A cross-reference was given with an unknown type."""
XREF_MISSING = "xref_missing"
"""A target was not found for a cross-reference."""
XREF_DUPLICATE = "xref_duplicate"
XREF_AMBIGUOUS = "xref_ambiguous"
"""Multiple targets were found for a cross-reference."""
XREF_NOT_EXPLICIT = "xref_not_explicit"
"""A target was not explicitly defined, and so may change in future."""
XREF_EMPTY = "xref_no_content"
"""No content was resolved for a cross-reference."""
XREF_PLACEHOLDER = "xref_replace"
"""Placeholders in a cross-reference text are missing."""
XREF_ANCHOR = "xref_anchor"
"""An anchor target is superseding other targets found for a cross-reference."""
XREF_ERROR = "xref_error"
"""An unspecified error occurred while resolving a cross-reference."""
IREF_MISSING = "iref_missing"
"""A target was not found for an inventory reference."""
IREF_DUPLICATE = "iref_duplicate"
IREF_AMBIGUOUS = "iref_ambiguous"
"""Multiple targets were found for an inventory reference."""
IREF_ERROR = "iref_error"
"""An unspecified error occurred while resolving an inventory reference."""
Expand All @@ -59,7 +61,7 @@ class MystWarnings(Enum):

# extensions
ANCHOR_DUPE = "anchor_dupe"
"""Duplicate target anchors found."""
"""Duplicate heading anchors generated in same document."""
STRIKETHROUGH = "strikethrough"
"""Strikethrough warning, since only implemented in HTML."""
HTML_PARSE = "html"
Expand Down
8 changes: 5 additions & 3 deletions tests/test_renderers/fixtures/myst_references.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ project_duplicate_local_first
<reference classes="std-label myst-project" internal="True" refid="index">
text

<src>/test.md:3: WARNING: Multiple targets found for '*:*:index': 'std:label:index','std:doc:index' [myst.xref_duplicate]
<src>/test.md:3: WARNING: Multiple targets found for '*:*:index': 'std:label:index','std:doc:index' [myst.xref_ambiguous]
.

project_duplicate_non_local
Expand All @@ -429,7 +429,7 @@ project_duplicate_non_local
<reference classes="std-label myst-project" internal="True" refuri="other.html#duplicate">
Other

<src>/test.md:2: WARNING: Multiple targets found for '*:*:duplicate': 'std:label:duplicate','std:term:duplicate' [myst.xref_duplicate]
<src>/test.md:2: WARNING: Multiple targets found for '*:*:duplicate': 'std:label:duplicate','std:term:duplicate' [myst.xref_ambiguous]
.

project_filter
Expand Down Expand Up @@ -566,7 +566,7 @@ myst_inv_duplicate [LOAD_INV]
<emphasis>
text

<src>/test.md:2: WARNING: Multiple targets found for '*:*:*:*modindex': 'project:std:label:modindex','project:std:label:py-modindex' [myst.iref_duplicate]
<src>/test.md:2: WARNING: Multiple targets found for '*:*:*:*modindex': 'project:std:label:modindex','project:std:label:py-modindex' [myst.iref_ambiguous]
.

implicit_anchors [ADD_ANCHORS]
Expand All @@ -589,6 +589,8 @@ implicit_anchors [ADD_ANCHORS]
<paragraph>
<reference classes="myst-anchor myst-project" internal="True" refid="a-title">
A Title

<src>/test.md:6: WARNING: 'a-title' anchor superseding other matches: 'std:label:a-title' [myst.xref_anchor]
.

unknown_uri
Expand Down

0 comments on commit 809144d

Please sign in to comment.