sphinx-java-autodoc 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,239 @@
1
+ """Java source documentation in Sphinx, without importing application code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+
10
+ from sphinx.errors import ExtensionError
11
+ from sphinx.util import logging
12
+
13
+ from .domain import JavaDomain
14
+ from .extractor import VERSION, atomic_write, extract
15
+ from .javadoc import check_mapping
16
+ from .model import (
17
+ MEMBER_GROUPS,
18
+ TYPE_GROUPS,
19
+ grouped,
20
+ local_name,
21
+ read_model,
22
+ use_model,
23
+ visible,
24
+ )
25
+ from .render import quiet_reference
26
+
27
+ __version__ = VERSION
28
+ LOGGER = logging.getLogger(__name__)
29
+ MARKER = ".. Generated by sphinx-java-autodoc; do not edit.\n\n"
30
+
31
+
32
+ def heading(title: str, underline: str) -> str:
33
+ return f"{title}\n{underline * len(title)}\n\n"
34
+
35
+
36
+ def summary_table(symbols) -> str:
37
+ return ".. java:autosummary::\n\n" + "".join(f" {s}\n" for s in symbols) + "\n"
38
+
39
+
40
+ def generate_pages(root, directory: Path, visibility: str) -> None:
41
+ pages = {}
42
+ entries = []
43
+ for package in root.findall("package"):
44
+ name = package.get("name", "")
45
+ if not re.fullmatch(r"[\w.$]*", name):
46
+ raise ExtensionError(f"Java autodoc: invalid package name {name!r}")
47
+ types = [
48
+ t
49
+ for t in root.findall("type")
50
+ if t.get("package") == name and visible(t, visibility)
51
+ ]
52
+ if not types:
53
+ continue
54
+ filename = "package-" + (name or "default")
55
+ entries.append((name, filename))
56
+ title = name or "Default package"
57
+ body = heading(title, "=")
58
+ if name:
59
+ body += f".. java:autopackage:: {name}\n\n"
60
+ for kind, group in TYPE_GROUPS.items():
61
+ symbols = [t.get("id") for t in types if t.get("kind") == kind]
62
+ if symbols:
63
+ body += heading(group, "-") + summary_table(symbols)
64
+ body += ".. toctree::\n :hidden:\n\n"
65
+ for t in types:
66
+ symbol = t.get("id")
67
+ if not re.fullmatch(r"[\w.$]+", symbol):
68
+ raise ExtensionError(f"Java autodoc: invalid type name {symbol!r}")
69
+ typefile = "type-" + symbol
70
+ body += f" {typefile}\n"
71
+ # Titles drop the package, which the sidebar and the signature already show.
72
+ page = heading(local_name(t), "=") + f".. java:autoclass:: {symbol}\n\n"
73
+ members = [m for m in t.findall("member") if visible(m, visibility)]
74
+ for group, found in grouped(members).items():
75
+ symbols = [m.get("id") for m in found]
76
+ directive = "autofield" if group == "field" else "automethod"
77
+ page += heading(MEMBER_GROUPS[group], "-") + summary_table(symbols)
78
+ page += "".join(f".. java:{directive}:: {s}\n\n" for s in symbols)
79
+ pages[typefile + ".rst"] = MARKER + page
80
+ pages[filename + ".rst"] = MARKER + body
81
+ named = [name for name, _ in entries if name]
82
+ pages["index.rst"] = (
83
+ MARKER
84
+ + heading("Java API", "=")
85
+ + (summary_table(named) if named else "")
86
+ # The summary cannot list the unnamed package, so then the toctree shows.
87
+ + (
88
+ ".. toctree::\n :hidden:\n\n"
89
+ if len(named) == len(entries)
90
+ else ".. toctree::\n\n"
91
+ )
92
+ + "".join(f" {filename}\n" for _, filename in entries)
93
+ )
94
+ manifest_path = directory / ".java-autodoc-manifest.json"
95
+ try:
96
+ old = json.loads(manifest_path.read_text()) if manifest_path.exists() else {}
97
+ if not isinstance(old, dict):
98
+ raise ValueError("expected a filename-to-hashes mapping")
99
+ for filename, hashes in old.items():
100
+ if isinstance(hashes, str):
101
+ hashes = old[filename] = [hashes]
102
+ if (
103
+ Path(filename).name != filename
104
+ or not filename.endswith(".rst")
105
+ or not isinstance(hashes, list)
106
+ or not hashes
107
+ or any(
108
+ not isinstance(h, str) or not re.fullmatch(r"[0-9a-f]{64}", h)
109
+ for h in hashes
110
+ )
111
+ ):
112
+ raise ValueError("invalid filename or content hash")
113
+ except (OSError, ValueError) as exc:
114
+ raise ExtensionError(
115
+ f"Java autodoc: invalid generated-page manifest: {exc}"
116
+ ) from exc
117
+ # Check ownership before recording any new files as ours.
118
+ for filename in old.keys() | pages.keys():
119
+ path = directory / filename
120
+ if path.exists() and hashlib.sha256(
121
+ path.read_bytes()
122
+ ).hexdigest() not in old.get(filename, []):
123
+ raise ExtensionError(
124
+ f"Java autodoc: refusing to overwrite edited or unowned page {path}; move your edits into a guide."
125
+ )
126
+ hashes = {
127
+ name: hashlib.sha256(content.encode()).hexdigest()
128
+ for name, content in pages.items()
129
+ }
130
+ pending = {name: set(values) for name, values in old.items()}
131
+ for name, digest in hashes.items():
132
+ pending.setdefault(name, set()).add(digest)
133
+ # Journal both versions before any page changes, so an interrupted build can retry.
134
+ atomic_write(
135
+ manifest_path,
136
+ json.dumps(
137
+ {name: sorted(values) for name, values in sorted(pending.items())}
138
+ ).encode(),
139
+ )
140
+ for filename, content in pages.items():
141
+ atomic_write(directory / filename, content.encode())
142
+ for filename in old.keys() - pages.keys():
143
+ (directory / filename).unlink(missing_ok=True)
144
+ atomic_write(manifest_path, json.dumps(hashes, indent=2, sort_keys=True).encode())
145
+
146
+
147
+ def prepare(app):
148
+ config = app.config
149
+ if config.java_visibility not in {"public", "protected"}:
150
+ raise ExtensionError(
151
+ "java_visibility must be 'public' or 'protected' (public plus protected)."
152
+ )
153
+ check_mapping(config.java_external_javadoc)
154
+ base = Path(app.confdir)
155
+ if config.java_model and config.java_sources:
156
+ raise ExtensionError("Set either java_sources or java_model, not both.")
157
+ if config.java_model:
158
+ path = (base / config.java_model).resolve()
159
+ root = read_model(path)
160
+ fingerprint = hashlib.sha256(path.read_bytes()).hexdigest()
161
+ elif config.java_sources:
162
+ cache = Path(app.doctreedir) / "java-cache"
163
+ path, fingerprint = extract(
164
+ [base / s for s in config.java_sources],
165
+ [base / s for s in config.java_classpath],
166
+ cache,
167
+ jdk=config.java_jdk,
168
+ release=str(config.java_release),
169
+ sourcepath=[base / s for s in config.java_sourcepath],
170
+ )
171
+ root = read_model(path)
172
+ # This cache belongs to one doctree directory, so only the current model is kept.
173
+ for old in cache.glob("*.xml"):
174
+ if old != path:
175
+ old.unlink(missing_ok=True)
176
+ else:
177
+ # A consumer using only Java roles and intersphinx needs no local sources.
178
+ import xml.etree.ElementTree as ET
179
+
180
+ root = ET.Element("api", schema="1")
181
+ path = base / "conf.py"
182
+ fingerprint = "empty"
183
+ app._java_changed = getattr(app.env, "java_fingerprint", None) != fingerprint
184
+ app.env.java_fingerprint = fingerprint
185
+ app.env.java_model_path = str(path)
186
+ use_model(root)
187
+ for diagnostic in root.findall("diagnostic"):
188
+ LOGGER.warning(diagnostic.text, type="java", subtype="extract")
189
+ if config.java_generate:
190
+ directory = (Path(app.srcdir) / config.java_generate).resolve()
191
+ if (
192
+ not directory.is_relative_to(Path(app.srcdir).resolve())
193
+ or directory == Path(app.srcdir).resolve()
194
+ ):
195
+ raise ExtensionError(
196
+ "java_generate must be a subdirectory of the Sphinx source directory."
197
+ )
198
+ if not root.findall("type"):
199
+ raise ExtensionError(
200
+ "Java autodoc: cannot generate API pages without extracted types."
201
+ )
202
+ generate_pages(root, directory, config.java_visibility)
203
+
204
+
205
+ def outdated(app, env, added, changed, removed):
206
+ # Roles and missing-type directives also depend on the complete Java model.
207
+ return sorted(env.found_docs - removed) if app._java_changed else []
208
+
209
+
210
+ def add_static_path(app):
211
+ app.config.html_static_path.append(str(Path(__file__).parent / "static"))
212
+
213
+
214
+ def setup(app):
215
+ for name, default in {
216
+ "java_sources": [],
217
+ "java_classpath": [],
218
+ "java_sourcepath": [],
219
+ "java_jdk": "",
220
+ "java_release": "",
221
+ "java_model": "",
222
+ "java_generate": "",
223
+ "java_visibility": "protected",
224
+ "java_external_javadoc": {},
225
+ "java_maximum_signature_line_length": None,
226
+ }.items():
227
+ app.add_config_value(name, default, "env")
228
+ app.add_domain(JavaDomain)
229
+ app.add_css_file("java-autodoc.css")
230
+ app.connect("builder-inited", add_static_path)
231
+ app.connect("builder-inited", prepare)
232
+ app.connect("env-get-outdated", outdated)
233
+ app.connect("missing-reference", quiet_reference)
234
+ return {
235
+ "version": VERSION,
236
+ "env_version": 4,
237
+ "parallel_read_safe": True,
238
+ "parallel_write_safe": True,
239
+ }
@@ -0,0 +1,67 @@
1
+ """Explicit extraction for build systems and JDK-free Sphinx rendering."""
2
+
3
+ import argparse
4
+ import os
5
+ from pathlib import Path
6
+ import sys
7
+
8
+ from sphinx.errors import ExtensionError
9
+
10
+ from .extractor import atomic_write, extract
11
+ from .model import read_model
12
+
13
+
14
+ def main():
15
+ parser = argparse.ArgumentParser(description=__doc__)
16
+ parser.add_argument(
17
+ "sources", nargs="+", type=Path, help="Java files or source roots"
18
+ )
19
+ parser.add_argument("-o", "--output", required=True, type=Path)
20
+ parser.add_argument(
21
+ "--classpath", default="", help=f"Compile classpath separated by {os.pathsep!r}"
22
+ )
23
+ parser.add_argument(
24
+ "--classpath-file",
25
+ type=Path,
26
+ help="UTF-8 file containing a platform-separated compile classpath",
27
+ )
28
+ parser.add_argument(
29
+ "--sourcepath",
30
+ action="append",
31
+ default=[],
32
+ type=Path,
33
+ help="Source lookup directory for ancestor docs; repeat for multiple roots",
34
+ )
35
+ parser.add_argument("--jdk", default="", help="JDK 25 home (default: java on PATH)")
36
+ parser.add_argument(
37
+ "--release", default="", help="Source/API release accepted by the installed JDK"
38
+ )
39
+ parser.add_argument("--cache", type=Path, default=Path(".java-autodoc-cache"))
40
+ parser.add_argument("--warnings-as-errors", action="store_true")
41
+ args = parser.parse_args()
42
+ try:
43
+ cp = args.classpath
44
+ if args.classpath_file:
45
+ cp += os.pathsep + args.classpath_file.read_text(encoding="utf-8").strip()
46
+ path, _ = extract(
47
+ args.sources,
48
+ [Path(p) for p in cp.split(os.pathsep) if p],
49
+ args.cache,
50
+ jdk=args.jdk,
51
+ release=args.release,
52
+ sourcepath=args.sourcepath,
53
+ )
54
+ diagnostics = read_model(path).findall("diagnostic")
55
+ for diagnostic in diagnostics:
56
+ print("warning: " + (diagnostic.text or ""), file=sys.stderr)
57
+ if diagnostics and args.warnings_as_errors:
58
+ return 1
59
+ atomic_write(args.output, path.read_bytes())
60
+ except (ExtensionError, OSError) as exc:
61
+ print(str(exc), file=sys.stderr)
62
+ return 1
63
+ return 0
64
+
65
+
66
+ if __name__ == "__main__":
67
+ sys.exit(main())
@@ -0,0 +1,387 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+
5
+ from docutils import nodes
6
+ from docutils.parsers.rst import directives
7
+ from sphinx import addnodes
8
+ from sphinx.domains import Domain, ObjType
9
+ from sphinx.roles import XRefRole
10
+ from sphinx.util import logging
11
+ from sphinx.util.docutils import SphinxDirective
12
+ from sphinx.util.nodes import make_refnode
13
+
14
+ from .javadoc import external_url
15
+ from .model import (
16
+ MEMBER_GROUPS,
17
+ PACKAGES,
18
+ REFERENCES,
19
+ SYMBOLS,
20
+ grouped,
21
+ local_name,
22
+ member_label,
23
+ visible,
24
+ )
25
+ from .render import deprecated, documentation, summary
26
+
27
+ LOGGER = logging.getLogger(__name__)
28
+
29
+
30
+ def anchor(symbol: str) -> str:
31
+ return "java-" + symbol.encode("utf-8").hex()
32
+
33
+
34
+ def role_for(element):
35
+ return (
36
+ "type"
37
+ if element.tag == "type"
38
+ else "method"
39
+ if element.get("kind") in {"method", "constructor"}
40
+ else "field"
41
+ )
42
+
43
+
44
+ # Signature tokens from the model mapped to Sphinx's standard signature nodes.
45
+ SIGNATURE_NODES = {
46
+ "k": addnodes.desc_sig_keyword,
47
+ "kt": addnodes.desc_sig_keyword_type,
48
+ "q": addnodes.desc_addname,
49
+ "n": addnodes.desc_name,
50
+ "tv": addnodes.desc_sig_name,
51
+ "pname": addnodes.desc_sig_name,
52
+ }
53
+
54
+
55
+ def signature_nodes(tokens) -> list[nodes.Node]:
56
+ result: list[nodes.Node] = []
57
+ for token in tokens:
58
+ text = token.text or ""
59
+ if token.tag == "params":
60
+ parameters = addnodes.desc_parameterlist()
61
+ for parameter in token.findall("param"):
62
+ parameters += addnodes.desc_parameter(
63
+ "", "", *signature_nodes(parameter)
64
+ )
65
+ result.append(parameters)
66
+ elif token.tag == "t":
67
+ for part in re.split(r"( +)", text):
68
+ if part:
69
+ result.append(
70
+ addnodes.desc_sig_space()
71
+ if part.isspace()
72
+ else addnodes.desc_sig_punctuation("", part)
73
+ )
74
+ elif token.tag == "v":
75
+ literal = (
76
+ addnodes.desc_sig_literal_string
77
+ if text[:1] in {'"', "'"}
78
+ else addnodes.desc_sig_literal_number
79
+ )
80
+ result.append(literal("", text))
81
+ elif token.tag == "r":
82
+ # Like type annotations in Sphinx's Python domain, unresolved types stay
83
+ # plain text and only warn in nitpicky mode.
84
+ reference = addnodes.pending_xref(
85
+ "",
86
+ nodes.Text(text),
87
+ refdomain="java",
88
+ reftype="type",
89
+ reftarget=token.get("target", ""),
90
+ refexplicit=True,
91
+ refwarn=False,
92
+ )
93
+ reference["java:external"] = dict(token.attrib)
94
+ result.append(reference)
95
+ elif token.tag in SIGNATURE_NODES:
96
+ result.append(SIGNATURE_NODES[token.tag]("", text))
97
+ return result
98
+
99
+
100
+ def display_name(symbol: str) -> str:
101
+ owner, _, member = symbol.partition("#")
102
+ return f"{owner}.{member_label(symbol)}" if member else symbol
103
+
104
+
105
+ class JavaDirective(SphinxDirective):
106
+ def visible(self, element) -> bool:
107
+ return visible(
108
+ element, self.options.get("visibility", self.config.java_visibility)
109
+ )
110
+
111
+ def lookup(self, symbol, tags=("type", "member")):
112
+ self.env.note_dependency(self.env.java_model_path)
113
+ element = SYMBOLS.get(symbol)
114
+ return element if element is not None and element.tag in tags else None
115
+
116
+ def missing(self, what, symbol, subtype):
117
+ LOGGER.warning(
118
+ "Java %s %s was not extracted; check java_sources and visibility.",
119
+ what,
120
+ symbol,
121
+ location=self.get_location(),
122
+ type="java",
123
+ subtype=subtype,
124
+ )
125
+
126
+ def describe(self, element):
127
+ symbol = element.get("id")
128
+ role = role_for(element)
129
+ desc = addnodes.desc(
130
+ "", domain="java", objtype=role, desctype=role, classes=["java", role]
131
+ )
132
+ sig = addnodes.desc_signature("", "", fullname=symbol)
133
+ sig.extend(signature_nodes(element.find("sig")))
134
+ limit = (
135
+ self.config.java_maximum_signature_line_length
136
+ or self.config.maximum_signature_line_length
137
+ )
138
+ if limit and len(sig.astext()) > limit:
139
+ for parameters in sig.findall(addnodes.desc_parameterlist):
140
+ parameters["multi_line_parameter_list"] = True
141
+ owner, _, member = symbol.partition("#")
142
+ # Sphinx lists objects in the page's table of contents from these attributes.
143
+ sig["_toc_parts"] = (owner, member) if member else (owner,)
144
+ sig["_toc_name"] = member_label(symbol) if member else owner.rpartition(".")[2]
145
+ result = [desc]
146
+ if "no-index" not in self.options:
147
+ target = anchor(symbol)
148
+ sig["ids"].append(target)
149
+ self.state.document.note_explicit_target(sig)
150
+ domain = self.env.get_domain("java")
151
+ if domain.data["objects"].get(symbol):
152
+ LOGGER.warning(
153
+ "Duplicate Java documentation for %s; use :no-index: on repeated excerpts.",
154
+ symbol,
155
+ location=self.get_location(),
156
+ type="java",
157
+ subtype="duplicate",
158
+ )
159
+ else:
160
+ domain.data["objects"][symbol] = (self.env.docname, target, role)
161
+ where = f" in {owner}" if member else ""
162
+ entry = f"{sig['_toc_name']} (Java {element.get('kind').replace('_', ' ')}{where})"
163
+ result.insert(
164
+ 0, addnodes.index(entries=[("single", entry, target, "", None)])
165
+ )
166
+ desc += sig
167
+ desc += addnodes.desc_content("", *documentation(element, self.get_location()))
168
+ return result
169
+
170
+
171
+ class AutoClass(JavaDirective):
172
+ required_arguments = 1
173
+ option_spec = {
174
+ "members": directives.unchanged,
175
+ "exclude-members": directives.unchanged,
176
+ "visibility": lambda v: directives.choice(v, ("public", "protected")),
177
+ "no-index": directives.flag,
178
+ }
179
+
180
+ def run(self):
181
+ symbol = self.arguments[0].strip()
182
+ element = self.lookup(symbol, ("type",))
183
+ if element is None:
184
+ self.missing("type", symbol, "type")
185
+ return []
186
+ if not self.visible(element):
187
+ return []
188
+ result = self.describe(element)
189
+ if "members" in self.options:
190
+ # Semicolons allow exact signatures containing commas.
191
+ selected = {
192
+ s.strip() for s in self.options["members"].split(";") if s.strip()
193
+ }
194
+ excluded = {
195
+ s.strip()
196
+ for s in self.options.get("exclude-members", "").split(";")
197
+ if s.strip()
198
+ }
199
+ matched = set()
200
+ members = []
201
+ for member in element.findall("member"):
202
+ names = {member.get("name"), member.get("id").split("#", 1)[1]}
203
+ if selected and not names & selected:
204
+ continue
205
+ matched.update(names & selected)
206
+ if names & excluded or not self.visible(member):
207
+ continue
208
+ members.append(member)
209
+ for missing in sorted(selected - matched):
210
+ LOGGER.warning(
211
+ "Java member %s#%s was not found.",
212
+ symbol,
213
+ missing,
214
+ location=self.get_location(),
215
+ type="java",
216
+ subtype="member",
217
+ )
218
+ content = result[-1][-1]
219
+ for group, found in grouped(members).items():
220
+ content += nodes.rubric("", MEMBER_GROUPS[group])
221
+ for member in found:
222
+ content.extend(self.describe(member))
223
+ return result
224
+
225
+
226
+ class AutoMember(JavaDirective):
227
+ """One method, constructor or field, like autodoc's automethod and autoattribute."""
228
+
229
+ required_arguments = 1
230
+ option_spec = {"no-index": directives.flag}
231
+
232
+ def run(self):
233
+ symbol = re.sub(r"\s+", "", self.arguments[0])
234
+ element = self.lookup(symbol.replace("...", "[]"), ("member",))
235
+ if element is None:
236
+ self.missing("member", symbol, "member")
237
+ return []
238
+ return self.describe(element)
239
+
240
+
241
+ class AutoPackage(JavaDirective):
242
+ required_arguments = 1
243
+
244
+ def run(self):
245
+ package = self.arguments[0].strip()
246
+ self.env.note_dependency(self.env.java_model_path)
247
+ element = PACKAGES.get(package)
248
+ if element is None:
249
+ self.missing("package", package, "package")
250
+ return []
251
+ domain = self.env.get_domain("java")
252
+ target = nodes.target("", "", ids=[anchor(package)])
253
+ self.state.document.note_explicit_target(target)
254
+ domain.data["objects"][package] = (self.env.docname, anchor(package), "package")
255
+ index = addnodes.index(
256
+ entries=[("single", f"{package} (Java package)", anchor(package), "", None)]
257
+ )
258
+ return [index, target, *documentation(element, self.get_location())]
259
+
260
+
261
+ class AutoSummary(JavaDirective):
262
+ """A table of linked names and first sentences, like sphinx.ext.autosummary."""
263
+
264
+ has_content = True
265
+
266
+ def run(self):
267
+ self.env.note_dependency(self.env.java_model_path)
268
+ table = nodes.table("", classes=["autosummary", "longtable", "java"])
269
+ group = nodes.tgroup("", cols=2)
270
+ group += nodes.colspec("", colwidth=10)
271
+ group += nodes.colspec("", colwidth=90)
272
+ body = nodes.tbody()
273
+ for line in self.content:
274
+ symbol = re.sub(r"\s+", "", line)
275
+ if not symbol:
276
+ continue
277
+ element = self.lookup(symbol)
278
+ if element is not None:
279
+ label = local_name(element)
280
+ elif symbol in PACKAGES:
281
+ element, label = PACKAGES[symbol], symbol
282
+ else:
283
+ self.missing("object", symbol, "summary")
284
+ continue
285
+ link = addnodes.pending_xref(
286
+ "",
287
+ nodes.literal("", label, classes=["xref", "java"]),
288
+ refdomain="java",
289
+ reftype="obj",
290
+ reftarget=symbol,
291
+ refexplicit=True,
292
+ refwarn=True,
293
+ )
294
+ text = nodes.paragraph("")
295
+ if deprecated(element):
296
+ text += [nodes.strong("", "Deprecated."), nodes.Text(" ")]
297
+ text.extend(summary(element, self.get_location()))
298
+ body += nodes.row(
299
+ "",
300
+ nodes.entry("", nodes.paragraph("", "", link)),
301
+ nodes.entry("", text),
302
+ )
303
+ if not body.children:
304
+ return []
305
+ group += body
306
+ table += group
307
+ return [table]
308
+
309
+
310
+ class JavaDomain(Domain):
311
+ name = "java"
312
+ label = "Java"
313
+ object_types = {
314
+ name: ObjType(name, name, "obj")
315
+ for name in ("type", "method", "field", "package")
316
+ }
317
+ roles = {
318
+ name: XRefRole(warn_dangling=True)
319
+ for name in ("type", "method", "field", "package", "obj")
320
+ }
321
+ directives = {
322
+ "autoclass": AutoClass,
323
+ "automethod": AutoMember,
324
+ "autofield": AutoMember,
325
+ "autopackage": AutoPackage,
326
+ "autosummary": AutoSummary,
327
+ }
328
+ initial_data = {"objects": {}}
329
+
330
+ def clear_doc(self, docname):
331
+ self.data["objects"] = {
332
+ k: v for k, v in self.data["objects"].items() if v[0] != docname
333
+ }
334
+
335
+ def merge_domaindata(self, docnames, otherdata):
336
+ for symbol, value in otherdata["objects"].items():
337
+ if value[0] in docnames:
338
+ if (
339
+ symbol in self.data["objects"]
340
+ and self.data["objects"][symbol] != value
341
+ ):
342
+ LOGGER.warning(
343
+ "Duplicate Java documentation for %s.",
344
+ symbol,
345
+ type="java",
346
+ subtype="duplicate",
347
+ )
348
+ self.data["objects"][symbol] = value
349
+
350
+ def get_objects(self):
351
+ for symbol, (docname, target, kind) in sorted(self.data["objects"].items()):
352
+ # Sphinx's search treats the text after the last dot as the object's name,
353
+ # so members display as Type.member(SimpleTypes); IDs stay canonical.
354
+ yield symbol, display_name(symbol), kind, docname, target, 0
355
+
356
+ def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
357
+ spelling = re.sub(r"\s+", "", target)
358
+ target = spelling.replace("...", "[]")
359
+ objects = self.data["objects"]
360
+ # Only member names can omit their parameter list.
361
+ if target not in objects and "#" in target and "(" not in target:
362
+ matches = [key for key in objects if key.startswith(target + "(")]
363
+ if len(matches) == 1:
364
+ target = matches[0]
365
+ elif len(matches) > 1:
366
+ LOGGER.warning(
367
+ "Ambiguous Java reference %s; include fully qualified parameter types.",
368
+ target,
369
+ location=node,
370
+ type="java",
371
+ subtype="ambiguous",
372
+ )
373
+ return None
374
+ if target in objects:
375
+ docname, ident, kind = objects[target]
376
+ if typ not in {"obj", kind}:
377
+ return None
378
+ return make_refnode(builder, fromdocname, docname, ident, contnode, target)
379
+ metadata = node.get("java:external") or REFERENCES.get(target, {})
380
+ url = external_url(target, spelling, metadata, env.config.java_external_javadoc)
381
+ return nodes.reference("", "", contnode, refuri=url) if url else None
382
+
383
+ def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode):
384
+ resolved = self.resolve_xref(
385
+ env, fromdocname, builder, "obj", target, node, contnode
386
+ )
387
+ return [("java:obj", resolved)] if resolved else []