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.
- sphinx_java_autodoc/__init__.py +239 -0
- sphinx_java_autodoc/__main__.py +67 -0
- sphinx_java_autodoc/domain.py +387 -0
- sphinx_java_autodoc/extractor.py +254 -0
- sphinx_java_autodoc/java/SphinxDoclet.java +1036 -0
- sphinx_java_autodoc/javadoc.py +45 -0
- sphinx_java_autodoc/model.py +173 -0
- sphinx_java_autodoc/render.py +469 -0
- sphinx_java_autodoc/static/java-autodoc.css +28 -0
- sphinx_java_autodoc-0.2.0.dist-info/METADATA +109 -0
- sphinx_java_autodoc-0.2.0.dist-info/RECORD +14 -0
- sphinx_java_autodoc-0.2.0.dist-info/WHEEL +4 -0
- sphinx_java_autodoc-0.2.0.dist-info/entry_points.txt +2 -0
- sphinx_java_autodoc-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1036 @@
|
|
|
1
|
+
package sphinxjava;
|
|
2
|
+
|
|
3
|
+
import com.sun.source.doctree.*;
|
|
4
|
+
import com.sun.source.util.DocTreePath;
|
|
5
|
+
import com.sun.source.util.DocTrees;
|
|
6
|
+
import java.nio.file.*;
|
|
7
|
+
import java.util.*;
|
|
8
|
+
import java.util.regex.Pattern;
|
|
9
|
+
import java.util.stream.Collectors;
|
|
10
|
+
import javax.lang.model.SourceVersion;
|
|
11
|
+
import javax.lang.model.element.*;
|
|
12
|
+
import javax.lang.model.type.*;
|
|
13
|
+
import javax.lang.model.util.Elements;
|
|
14
|
+
import javax.lang.model.util.Types;
|
|
15
|
+
import javax.tools.Diagnostic;
|
|
16
|
+
import javax.xml.parsers.DocumentBuilderFactory;
|
|
17
|
+
import javax.xml.transform.*;
|
|
18
|
+
import javax.xml.transform.dom.DOMSource;
|
|
19
|
+
import javax.xml.transform.stream.StreamResult;
|
|
20
|
+
import jdk.javadoc.doclet.*;
|
|
21
|
+
|
|
22
|
+
/** Extracts source declarations and semantic documentation using public JDK APIs. */
|
|
23
|
+
public final class SphinxDoclet implements Doclet {
|
|
24
|
+
private static final String THROWS = "throws:";
|
|
25
|
+
// shortcut: conservatively detect markup comments; replace with a markup parser when
|
|
26
|
+
// supporting regions/actions.
|
|
27
|
+
private static final Pattern SNIPPET_MARKUP =
|
|
28
|
+
Pattern.compile("(?s).*(?://|#)[^\\r\\n]*@(?:start|end|highlight|replace|link)\\b.*");
|
|
29
|
+
|
|
30
|
+
private Reporter reporter;
|
|
31
|
+
private DocTrees trees;
|
|
32
|
+
private Elements elements;
|
|
33
|
+
private Types types;
|
|
34
|
+
private Path output;
|
|
35
|
+
private final Set<String> warnings = new TreeSet<>();
|
|
36
|
+
private final Map<Element, Map<String, Part>> comments = new HashMap<>();
|
|
37
|
+
|
|
38
|
+
/** Documentation content together with the declaration whose comment supplied it. */
|
|
39
|
+
private record Part(Element owner, String label, List<? extends DocTree> trees) {}
|
|
40
|
+
|
|
41
|
+
/** Fluent builder for the model's DOM elements; all values are made XML 1.0 safe. */
|
|
42
|
+
private record Xml(org.w3c.dom.Element node) {
|
|
43
|
+
Xml add(String tag) {
|
|
44
|
+
var child = node.getOwnerDocument().createElement(tag);
|
|
45
|
+
node.appendChild(child);
|
|
46
|
+
return new Xml(child);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Xml set(String name, String value) {
|
|
50
|
+
node.setAttribute(name, safe(value));
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
void addText(String tag, String value) {
|
|
55
|
+
var child = add(tag).node;
|
|
56
|
+
child.appendChild(child.getOwnerDocument().createTextNode(safe(value)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
void addAttr(String name, String value) {
|
|
60
|
+
add("attr").set("name", name).node.setTextContent(safe(value));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Writes characters that XML 1.0 cannot contain (controls, lone surrogates) as Java Unicode
|
|
65
|
+
* escapes. javac translates Unicode escapes even inside comments, so a NUL in a comment was
|
|
66
|
+
* written as an escape in the source; this restores that spelling.
|
|
67
|
+
*/
|
|
68
|
+
private static String safe(String value) {
|
|
69
|
+
if (value.codePoints().allMatch(Xml::allowed)) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
var result = new StringBuilder();
|
|
73
|
+
value
|
|
74
|
+
.codePoints()
|
|
75
|
+
.forEach(
|
|
76
|
+
c -> {
|
|
77
|
+
if (allowed(c)) {
|
|
78
|
+
result.appendCodePoint(c);
|
|
79
|
+
} else {
|
|
80
|
+
result.append("\\u%04x".formatted(c));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return result.toString();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private static boolean allowed(int c) {
|
|
87
|
+
return c == '\t'
|
|
88
|
+
|| c == '\n'
|
|
89
|
+
|| c == '\r'
|
|
90
|
+
|| (c >= 0x20 && c <= 0xD7FF)
|
|
91
|
+
|| (c >= 0xE000 && c <= 0xFFFD)
|
|
92
|
+
|| c >= 0x10000;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Override
|
|
97
|
+
public void init(Locale locale, Reporter reporter) {
|
|
98
|
+
this.reporter = reporter;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@Override
|
|
102
|
+
public String getName() {
|
|
103
|
+
return "SphinxDoclet";
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@Override
|
|
107
|
+
public SourceVersion getSupportedSourceVersion() {
|
|
108
|
+
return SourceVersion.latestSupported();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@Override
|
|
112
|
+
public Set<? extends Option> getSupportedOptions() {
|
|
113
|
+
return Set.of(
|
|
114
|
+
new Option() {
|
|
115
|
+
@Override
|
|
116
|
+
public int getArgumentCount() {
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Override
|
|
121
|
+
public String getDescription() {
|
|
122
|
+
return "Write versioned Sphinx API XML";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Override
|
|
126
|
+
public Kind getKind() {
|
|
127
|
+
return Kind.STANDARD;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
@Override
|
|
131
|
+
public List<String> getNames() {
|
|
132
|
+
return List.of("--sphinx-output");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@Override
|
|
136
|
+
public String getParameters() {
|
|
137
|
+
return "file";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Override
|
|
141
|
+
public boolean process(String name, List<String> arguments) {
|
|
142
|
+
output = Path.of(arguments.getFirst());
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Override
|
|
149
|
+
public boolean run(DocletEnvironment env) {
|
|
150
|
+
trees = env.getDocTrees();
|
|
151
|
+
elements = env.getElementUtils();
|
|
152
|
+
types = env.getTypeUtils();
|
|
153
|
+
if (output == null) {
|
|
154
|
+
reporter.print(Diagnostic.Kind.ERROR, "--sphinx-output is required");
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
var document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
|
|
159
|
+
var root = new Xml(document.createElement("api"));
|
|
160
|
+
document.appendChild(root.node());
|
|
161
|
+
// extractor.py stamps the package version, so the doclet has no copy to keep in sync.
|
|
162
|
+
root.set("schema", "1");
|
|
163
|
+
var included =
|
|
164
|
+
env.getIncludedElements().stream()
|
|
165
|
+
.filter(e -> e instanceof TypeElement && visible(e, false) && !hidden(e))
|
|
166
|
+
.map(TypeElement.class::cast)
|
|
167
|
+
.sorted(Comparator.comparing(t -> t.getQualifiedName().toString()))
|
|
168
|
+
.toList();
|
|
169
|
+
var packages = new TreeMap<String, PackageElement>();
|
|
170
|
+
for (var type : included) {
|
|
171
|
+
var pkg = elements.getPackageOf(type);
|
|
172
|
+
packages.put(pkg.toString(), pkg);
|
|
173
|
+
}
|
|
174
|
+
for (var pkg : packages.values()) {
|
|
175
|
+
documentation(root.add("package").set("name", pkg.getQualifiedName().toString()), pkg);
|
|
176
|
+
}
|
|
177
|
+
for (var type : included) {
|
|
178
|
+
var node = declaration(root, "type", type);
|
|
179
|
+
type.getEnclosedElements().stream()
|
|
180
|
+
.filter(e -> e instanceof ExecutableElement || e instanceof VariableElement)
|
|
181
|
+
.filter(e -> visible(e, false) && !hidden(e))
|
|
182
|
+
.sorted(Comparator.comparing(this::id))
|
|
183
|
+
.forEach(member -> declaration(node, "member", member));
|
|
184
|
+
}
|
|
185
|
+
warnings.forEach(warning -> root.addText("diagnostic", warning));
|
|
186
|
+
var transformer = TransformerFactory.newInstance().newTransformer();
|
|
187
|
+
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
|
188
|
+
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
|
189
|
+
transformer.transform(new DOMSource(document), new StreamResult(output.toFile()));
|
|
190
|
+
return true;
|
|
191
|
+
} catch (Exception e) {
|
|
192
|
+
reporter.print(Diagnostic.Kind.ERROR, "Sphinx extraction failed: " + e);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Whether the declaration and all enclosing declarations are public (or protected). */
|
|
198
|
+
private static boolean visible(Element e, boolean publicOnly) {
|
|
199
|
+
while (e instanceof TypeElement
|
|
200
|
+
|| e instanceof ExecutableElement
|
|
201
|
+
|| e instanceof VariableElement) {
|
|
202
|
+
var modifiers = e.getModifiers();
|
|
203
|
+
boolean exported =
|
|
204
|
+
modifiers.contains(Modifier.PUBLIC)
|
|
205
|
+
|| (!publicOnly && modifiers.contains(Modifier.PROTECTED));
|
|
206
|
+
if (!exported) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
e = e.getEnclosingElement();
|
|
210
|
+
}
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private boolean hidden(Element e) {
|
|
215
|
+
var comment = trees.getDocCommentTree(e);
|
|
216
|
+
return comment != null
|
|
217
|
+
&& comment.getBlockTags().stream().anyMatch(t -> t.getKind() == DocTree.Kind.HIDDEN);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Records a diagnostic; {@code template} is a {@link String#format} pattern. */
|
|
221
|
+
private void warn(Element owner, String template, Object... args) {
|
|
222
|
+
warnings.add(id(owner) + ": " + (args.length == 0 ? template : template.formatted(args)));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Declarations and signatures
|
|
226
|
+
|
|
227
|
+
private String id(Element e) {
|
|
228
|
+
if (e instanceof QualifiedNameable named) {
|
|
229
|
+
return named.getQualifiedName().toString();
|
|
230
|
+
}
|
|
231
|
+
String id = id(e.getEnclosingElement()) + "#" + e.getSimpleName();
|
|
232
|
+
if (e instanceof ExecutableElement method) {
|
|
233
|
+
id +=
|
|
234
|
+
method.getParameters().stream()
|
|
235
|
+
.map(p -> linkType(types.erasure(p.asType())))
|
|
236
|
+
.collect(Collectors.joining(",", "(", ")"));
|
|
237
|
+
}
|
|
238
|
+
return id;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private static String linkType(TypeMirror type) {
|
|
242
|
+
return switch (type) {
|
|
243
|
+
case ArrayType array -> linkType(array.getComponentType()) + "[]";
|
|
244
|
+
case TypeVariable variable -> variable.asElement().getSimpleName().toString();
|
|
245
|
+
case DeclaredType declared ->
|
|
246
|
+
((TypeElement) declared.asElement()).getQualifiedName().toString();
|
|
247
|
+
default ->
|
|
248
|
+
type.getKind().isPrimitive()
|
|
249
|
+
? type.getKind().name().toLowerCase(Locale.ROOT)
|
|
250
|
+
: type.toString();
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Structured signatures: the same declaration as tokens, so renderers can link types and
|
|
255
|
+
// build native signature nodes. Tags: k keyword, kt primitive type, t punctuation or space,
|
|
256
|
+
// q qualifier, n declared name, tv type variable, pname parameter name, r type reference,
|
|
257
|
+
// v constant value, params/param a parameter list.
|
|
258
|
+
|
|
259
|
+
private void sig(Xml parent, Element e) {
|
|
260
|
+
var sig = parent.add("sig");
|
|
261
|
+
for (var annotation : e.getAnnotationMirrors()) {
|
|
262
|
+
annotation(sig, annotation);
|
|
263
|
+
sig.addText("t", " ");
|
|
264
|
+
}
|
|
265
|
+
for (var modifier : e.getModifiers()) {
|
|
266
|
+
sig.addText("k", modifier.toString());
|
|
267
|
+
sig.addText("t", " ");
|
|
268
|
+
}
|
|
269
|
+
switch (e) {
|
|
270
|
+
case TypeElement type -> typeSig(sig, type);
|
|
271
|
+
case ExecutableElement method -> methodSig(sig, method);
|
|
272
|
+
case VariableElement variable -> {
|
|
273
|
+
typeTokens(sig, variable.asType());
|
|
274
|
+
sig.addText("t", " ");
|
|
275
|
+
sig.addText("n", variable.getSimpleName().toString());
|
|
276
|
+
var value = variable.getConstantValue();
|
|
277
|
+
if (value != null) {
|
|
278
|
+
sig.addText("t", " = ");
|
|
279
|
+
sig.addText("v", elements.getConstantExpression(value));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
default -> throw new IllegalArgumentException("Unexpected declaration " + e);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private void typeSig(Xml sig, TypeElement type) {
|
|
287
|
+
var kind = type.getKind();
|
|
288
|
+
sig.addText(
|
|
289
|
+
"k",
|
|
290
|
+
kind == ElementKind.ANNOTATION_TYPE ? "@interface" : kind.name().toLowerCase(Locale.ROOT));
|
|
291
|
+
sig.addText("t", " ");
|
|
292
|
+
String pkg = elements.getPackageOf(type).getQualifiedName().toString();
|
|
293
|
+
String name = type.getQualifiedName().toString();
|
|
294
|
+
if (!pkg.isEmpty()) {
|
|
295
|
+
sig.addText("q", pkg + ".");
|
|
296
|
+
name = name.substring(pkg.length() + 1);
|
|
297
|
+
}
|
|
298
|
+
sig.addText("n", name);
|
|
299
|
+
typeParameterTokens(sig, type.getTypeParameters());
|
|
300
|
+
if (kind == ElementKind.RECORD) {
|
|
301
|
+
var params = sig.add("params");
|
|
302
|
+
for (var component : type.getRecordComponents()) {
|
|
303
|
+
var param = params.add("param");
|
|
304
|
+
for (var annotation : component.getAnnotationMirrors()) {
|
|
305
|
+
annotation(param, annotation);
|
|
306
|
+
param.addText("t", " ");
|
|
307
|
+
}
|
|
308
|
+
typeTokens(param, component.asType());
|
|
309
|
+
param.addText("t", " ");
|
|
310
|
+
param.addText("pname", component.getSimpleName().toString());
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
var superclass = type.getSuperclass();
|
|
314
|
+
if (kind == ElementKind.CLASS
|
|
315
|
+
&& superclass.getKind() != TypeKind.NONE
|
|
316
|
+
&& !superclass.toString().equals("java.lang.Object")) {
|
|
317
|
+
clause(sig, "extends", List.of(superclass));
|
|
318
|
+
}
|
|
319
|
+
clause(sig, kind.isInterface() ? "extends" : "implements", type.getInterfaces());
|
|
320
|
+
clause(sig, "permits", type.getPermittedSubclasses());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private void methodSig(Xml sig, ExecutableElement method) {
|
|
324
|
+
if (!method.getTypeParameters().isEmpty()) {
|
|
325
|
+
typeParameterTokens(sig, method.getTypeParameters());
|
|
326
|
+
sig.addText("t", " ");
|
|
327
|
+
}
|
|
328
|
+
if (method.getKind() == ElementKind.CONSTRUCTOR) {
|
|
329
|
+
sig.addText("n", method.getEnclosingElement().getSimpleName().toString());
|
|
330
|
+
} else {
|
|
331
|
+
typeTokens(sig, method.getReturnType());
|
|
332
|
+
sig.addText("t", " ");
|
|
333
|
+
sig.addText("n", method.getSimpleName().toString());
|
|
334
|
+
}
|
|
335
|
+
var params = sig.add("params");
|
|
336
|
+
var parameters = method.getParameters();
|
|
337
|
+
for (int i = 0; i < parameters.size(); i++) {
|
|
338
|
+
var parameter = parameters.get(i);
|
|
339
|
+
var param = params.add("param");
|
|
340
|
+
for (var annotation : parameter.getAnnotationMirrors()) {
|
|
341
|
+
annotation(param, annotation);
|
|
342
|
+
param.addText("t", " ");
|
|
343
|
+
}
|
|
344
|
+
if (method.isVarArgs()
|
|
345
|
+
&& i == parameters.size() - 1
|
|
346
|
+
&& parameter.asType() instanceof ArrayType array) {
|
|
347
|
+
typeTokens(param, array.getComponentType());
|
|
348
|
+
param.addText("t", "...");
|
|
349
|
+
} else {
|
|
350
|
+
typeTokens(param, parameter.asType());
|
|
351
|
+
}
|
|
352
|
+
param.addText("t", " ");
|
|
353
|
+
param.addText("pname", parameter.getSimpleName().toString());
|
|
354
|
+
}
|
|
355
|
+
clause(sig, "throws", method.getThrownTypes());
|
|
356
|
+
if (method.getDefaultValue() != null) {
|
|
357
|
+
sig.addText("t", " ");
|
|
358
|
+
sig.addText("k", "default");
|
|
359
|
+
sig.addText("t", " ");
|
|
360
|
+
sig.addText("v", method.getDefaultValue().toString());
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private void clause(Xml sig, String keyword, List<? extends TypeMirror> types) {
|
|
365
|
+
if (types.isEmpty()) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
sig.addText("t", " ");
|
|
369
|
+
sig.addText("k", keyword);
|
|
370
|
+
sig.addText("t", " ");
|
|
371
|
+
for (int i = 0; i < types.size(); i++) {
|
|
372
|
+
if (i > 0) {
|
|
373
|
+
sig.addText("t", ", ");
|
|
374
|
+
}
|
|
375
|
+
typeTokens(sig, types.get(i));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
private void typeParameterTokens(Xml sig, List<? extends TypeParameterElement> parameters) {
|
|
380
|
+
if (parameters.isEmpty()) {
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
sig.addText("t", "<");
|
|
384
|
+
for (int i = 0; i < parameters.size(); i++) {
|
|
385
|
+
if (i > 0) {
|
|
386
|
+
sig.addText("t", ", ");
|
|
387
|
+
}
|
|
388
|
+
var parameter = parameters.get(i);
|
|
389
|
+
sig.addText("tv", parameter.getSimpleName().toString());
|
|
390
|
+
var bounds = parameter.getBounds();
|
|
391
|
+
if (bounds.size() == 1 && bounds.getFirst().toString().equals("java.lang.Object")) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
sig.addText("t", " ");
|
|
395
|
+
sig.addText("k", "extends");
|
|
396
|
+
sig.addText("t", " ");
|
|
397
|
+
for (int b = 0; b < bounds.size(); b++) {
|
|
398
|
+
if (b > 0) {
|
|
399
|
+
sig.addText("t", " & ");
|
|
400
|
+
}
|
|
401
|
+
typeTokens(sig, bounds.get(b));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
sig.addText("t", ">");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private void typeTokens(Xml sig, TypeMirror type) {
|
|
408
|
+
for (var annotation : type.getAnnotationMirrors()) {
|
|
409
|
+
annotation(sig, annotation);
|
|
410
|
+
sig.addText("t", " ");
|
|
411
|
+
}
|
|
412
|
+
switch (type) {
|
|
413
|
+
case ArrayType array -> {
|
|
414
|
+
typeTokens(sig, array.getComponentType());
|
|
415
|
+
sig.addText("t", "[]");
|
|
416
|
+
}
|
|
417
|
+
case DeclaredType declared -> {
|
|
418
|
+
typeReference(sig, (TypeElement) declared.asElement());
|
|
419
|
+
var arguments = declared.getTypeArguments();
|
|
420
|
+
if (!arguments.isEmpty()) {
|
|
421
|
+
sig.addText("t", "<");
|
|
422
|
+
for (int i = 0; i < arguments.size(); i++) {
|
|
423
|
+
if (i > 0) {
|
|
424
|
+
sig.addText("t", ", ");
|
|
425
|
+
}
|
|
426
|
+
typeTokens(sig, arguments.get(i));
|
|
427
|
+
}
|
|
428
|
+
sig.addText("t", ">");
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
case WildcardType wildcard -> {
|
|
432
|
+
sig.addText("t", "?");
|
|
433
|
+
if (wildcard.getExtendsBound() != null) {
|
|
434
|
+
clause(sig, "extends", List.of(wildcard.getExtendsBound()));
|
|
435
|
+
} else if (wildcard.getSuperBound() != null) {
|
|
436
|
+
clause(sig, "super", List.of(wildcard.getSuperBound()));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
case TypeVariable variable ->
|
|
440
|
+
sig.addText("tv", variable.asElement().getSimpleName().toString());
|
|
441
|
+
case IntersectionType intersection -> {
|
|
442
|
+
var bounds = intersection.getBounds();
|
|
443
|
+
for (int i = 0; i < bounds.size(); i++) {
|
|
444
|
+
if (i > 0) {
|
|
445
|
+
sig.addText("t", " & ");
|
|
446
|
+
}
|
|
447
|
+
typeTokens(sig, bounds.get(i));
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
default ->
|
|
451
|
+
sig.addText(
|
|
452
|
+
"kt",
|
|
453
|
+
type.getKind().isPrimitive() || type.getKind() == TypeKind.VOID
|
|
454
|
+
? type.getKind().name().toLowerCase(Locale.ROOT)
|
|
455
|
+
: type.toString());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/** A link to a type: its compiler ID, external Javadoc location and nested simple name. */
|
|
460
|
+
private void typeReference(Xml sig, TypeElement type) {
|
|
461
|
+
String pkg = elements.getPackageOf(type).getQualifiedName().toString();
|
|
462
|
+
String name = type.getQualifiedName().toString();
|
|
463
|
+
var reference = sig.add("r").set("target", id(type));
|
|
464
|
+
javadocLocation(reference, type);
|
|
465
|
+
reference
|
|
466
|
+
.node()
|
|
467
|
+
.setTextContent(Xml.safe(pkg.isEmpty() ? name : name.substring(pkg.length() + 1)));
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private void annotation(Xml sig, AnnotationMirror annotation) {
|
|
471
|
+
sig.addText("t", "@");
|
|
472
|
+
typeReference(sig, (TypeElement) annotation.getAnnotationType().asElement());
|
|
473
|
+
var values = annotation.getElementValues();
|
|
474
|
+
if (values.isEmpty()) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
String arguments =
|
|
478
|
+
values.size() == 1
|
|
479
|
+
&& values.keySet().iterator().next().getSimpleName().contentEquals("value")
|
|
480
|
+
? values.values().iterator().next().toString()
|
|
481
|
+
: values.entrySet().stream()
|
|
482
|
+
.map(v -> v.getKey().getSimpleName() + "=" + v.getValue())
|
|
483
|
+
.collect(Collectors.joining(", "));
|
|
484
|
+
sig.addText("t", "(" + arguments + ")");
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private Xml declaration(Xml parent, String tag, Element e) {
|
|
488
|
+
var node =
|
|
489
|
+
parent
|
|
490
|
+
.add(tag)
|
|
491
|
+
.set("id", id(e))
|
|
492
|
+
.set("name", e.getSimpleName().toString())
|
|
493
|
+
.set("kind", e.getKind().name().toLowerCase(Locale.ROOT))
|
|
494
|
+
.set("visibility", visible(e, true) ? "public" : "protected")
|
|
495
|
+
.set("package", elements.getPackageOf(e).getQualifiedName().toString());
|
|
496
|
+
sig(node, e);
|
|
497
|
+
documentation(node, e);
|
|
498
|
+
return node;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Comment parts and inheritance
|
|
502
|
+
|
|
503
|
+
/** Returns the declaration's own comment parts, keyed by kind and parameter position. */
|
|
504
|
+
private Map<String, Part> parts(Element e) {
|
|
505
|
+
var cached = comments.get(e);
|
|
506
|
+
if (cached != null) {
|
|
507
|
+
return cached;
|
|
508
|
+
}
|
|
509
|
+
var result = new LinkedHashMap<String, Part>();
|
|
510
|
+
comments.put(e, result);
|
|
511
|
+
var comment = trees.getDocCommentTree(e);
|
|
512
|
+
if (comment == null) {
|
|
513
|
+
return result;
|
|
514
|
+
}
|
|
515
|
+
if (elements.getDocCommentKind(e) == Elements.DocCommentKind.END_OF_LINE) {
|
|
516
|
+
warn(
|
|
517
|
+
e,
|
|
518
|
+
"Markdown documentation comments are not supported; use traditional /** comments for"
|
|
519
|
+
+ " this release.");
|
|
520
|
+
}
|
|
521
|
+
if (!comment.getFullBody().isEmpty()) {
|
|
522
|
+
result.put("body", new Part(e, "", comment.getFullBody()));
|
|
523
|
+
if (comment.getFullBody().getFirst() instanceof ReturnTree ret && ret.isInline()) {
|
|
524
|
+
result.put("return", new Part(e, "", ret.getDescription()));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
int see = 0;
|
|
528
|
+
for (var tag : comment.getBlockTags()) {
|
|
529
|
+
switch (tag) {
|
|
530
|
+
case ParamTree param -> putParam(result, e, param);
|
|
531
|
+
case ReturnTree ret -> result.put("return", new Part(e, "", ret.getDescription()));
|
|
532
|
+
case ThrowsTree thrown -> putThrows(result, e, thrown);
|
|
533
|
+
case SeeTree s -> result.put("see:" + see++, new Part(e, "", s.getReference()));
|
|
534
|
+
case SinceTree since -> result.put("since", new Part(e, "", since.getBody()));
|
|
535
|
+
case DeprecatedTree d -> result.put("deprecated", new Part(e, "", d.getBody()));
|
|
536
|
+
default -> {
|
|
537
|
+
if (tag.getKind() != DocTree.Kind.HIDDEN) {
|
|
538
|
+
warn(e, "Unsupported block tag %s; retain it in handwritten guides.", tag.getKind());
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private void putParam(Map<String, Part> result, Element e, ParamTree param) {
|
|
547
|
+
String name = param.getName().toString();
|
|
548
|
+
String key = "param:" + name;
|
|
549
|
+
if (e instanceof ExecutableElement method) {
|
|
550
|
+
var declared = param.isTypeParameter() ? method.getTypeParameters() : method.getParameters();
|
|
551
|
+
int index = -1;
|
|
552
|
+
for (int i = 0; i < declared.size(); i++) {
|
|
553
|
+
if (declared.get(i).getSimpleName().contentEquals(name)) {
|
|
554
|
+
index = i;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (index < 0) {
|
|
558
|
+
warn(e, "@param %s does not name a declared parameter.", name);
|
|
559
|
+
}
|
|
560
|
+
key = (param.isTypeParameter() ? "typeparam:" : "param:") + index;
|
|
561
|
+
}
|
|
562
|
+
String label = param.isTypeParameter() ? "<" + name + ">" : name;
|
|
563
|
+
result.put(key, new Part(e, label, param.getDescription()));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
private void putThrows(Map<String, Part> result, Element e, ThrowsTree thrown) {
|
|
567
|
+
var target = resolve(e, thrown.getExceptionName());
|
|
568
|
+
String name = target == null ? thrown.getExceptionName().toString() : id(target);
|
|
569
|
+
// Inheritance must copy every description of the same exception.
|
|
570
|
+
result.merge(
|
|
571
|
+
THROWS + name,
|
|
572
|
+
new Part(e, name, thrown.getDescription()),
|
|
573
|
+
(first, next) -> {
|
|
574
|
+
var paragraph =
|
|
575
|
+
trees
|
|
576
|
+
.getDocTreeFactory()
|
|
577
|
+
.newStartElementTree(elements.getName("p"), List.of(), false);
|
|
578
|
+
var content = new ArrayList<DocTree>(first.trees());
|
|
579
|
+
content.add(paragraph);
|
|
580
|
+
content.addAll(next.trees());
|
|
581
|
+
return new Part(e, name, content);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private ExecutableElement overridden(ExecutableElement method) {
|
|
586
|
+
var owner = (TypeElement) method.getEnclosingElement();
|
|
587
|
+
var result = search(method, owner, owner, new HashSet<>());
|
|
588
|
+
if (result != null) {
|
|
589
|
+
return result;
|
|
590
|
+
}
|
|
591
|
+
return search(method, owner, elements.getTypeElement("java.lang.Object"), new HashSet<>());
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private ExecutableElement search(
|
|
595
|
+
ExecutableElement method, TypeElement owner, TypeElement current, Set<TypeElement> seen) {
|
|
596
|
+
if (!seen.add(current)) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
for (var e : current.getEnclosedElements()) {
|
|
600
|
+
if (e instanceof ExecutableElement candidate
|
|
601
|
+
&& candidate.getKind() == ElementKind.METHOD
|
|
602
|
+
&& elements.overrides(method, candidate, owner)) {
|
|
603
|
+
return candidate;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
if (types.asElement(current.getSuperclass()) instanceof TypeElement superclass
|
|
607
|
+
&& !superclass.getQualifiedName().contentEquals("java.lang.Object")) {
|
|
608
|
+
var found = search(method, owner, superclass, seen);
|
|
609
|
+
if (found != null) {
|
|
610
|
+
return found;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
for (var type : current.getInterfaces()) {
|
|
614
|
+
var found = search(method, owner, (TypeElement) types.asElement(type), seen);
|
|
615
|
+
if (found != null) {
|
|
616
|
+
return found;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Maps a child's throws key to the ancestor's key, following generic substitution. */
|
|
623
|
+
private String inheritedKey(ExecutableElement child, ExecutableElement ancestor, String key) {
|
|
624
|
+
if (!key.startsWith(THROWS)) {
|
|
625
|
+
return key;
|
|
626
|
+
}
|
|
627
|
+
TypeMirror requested = null;
|
|
628
|
+
for (var type : child.getThrownTypes()) {
|
|
629
|
+
if (key.equals(THROWS + id(types.asElement(type)))) {
|
|
630
|
+
requested = type;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (requested == null
|
|
634
|
+
&& elements.getTypeElement(key.substring(THROWS.length())) instanceof TypeElement type) {
|
|
635
|
+
requested = type.asType();
|
|
636
|
+
}
|
|
637
|
+
if (requested == null) {
|
|
638
|
+
return key;
|
|
639
|
+
}
|
|
640
|
+
var owner = (DeclaredType) child.getEnclosingElement().asType();
|
|
641
|
+
var member = (ExecutableType) types.asMemberOf(owner, ancestor);
|
|
642
|
+
String subtypeMatch = key;
|
|
643
|
+
for (int i = 0; i < ancestor.getThrownTypes().size(); i++) {
|
|
644
|
+
if (!(ancestor.getThrownTypes().get(i) instanceof TypeVariable declared)) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
var substituted = member.getThrownTypes().get(i);
|
|
648
|
+
if (declared.asElement().getEnclosingElement().equals(ancestor)
|
|
649
|
+
&& child.getTypeParameters().size() == ancestor.getTypeParameters().size()) {
|
|
650
|
+
int position = ancestor.getTypeParameters().indexOf(declared.asElement());
|
|
651
|
+
substituted = child.getTypeParameters().get(position).asType();
|
|
652
|
+
}
|
|
653
|
+
String candidate = THROWS + id(types.asElement(declared));
|
|
654
|
+
if (types.isSameType(requested, substituted)) {
|
|
655
|
+
return candidate;
|
|
656
|
+
}
|
|
657
|
+
if (subtypeMatch.equals(key) && types.isSubtype(requested, substituted)) {
|
|
658
|
+
subtypeMatch = candidate;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return subtypeMatch;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private Part findPart(Element e, String key, Set<Element> seen) {
|
|
665
|
+
if (!seen.add(e)) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
Part part = parts(e).get(key);
|
|
669
|
+
if (part != null && !part.trees().isEmpty()) {
|
|
670
|
+
return part;
|
|
671
|
+
}
|
|
672
|
+
if (key.startsWith(THROWS)) {
|
|
673
|
+
var requested = elements.getTypeElement(key.substring(THROWS.length()));
|
|
674
|
+
for (var entry : parts(e).entrySet()) {
|
|
675
|
+
if (!entry.getKey().startsWith(THROWS)) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
var documented = elements.getTypeElement(entry.getKey().substring(THROWS.length()));
|
|
679
|
+
if (requested != null
|
|
680
|
+
&& documented != null
|
|
681
|
+
&& types.isSubtype(requested.asType(), documented.asType())) {
|
|
682
|
+
return entry.getValue();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (e instanceof ExecutableElement method && e.getKind() == ElementKind.METHOD) {
|
|
687
|
+
var parent = overridden(method);
|
|
688
|
+
if (parent != null) {
|
|
689
|
+
return findPart(parent, inheritedKey(method, parent, key), seen);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Returns the method if it is declared in source and overrides an ancestor, else null. */
|
|
696
|
+
private ExecutableElement overridingMethod(Element e) {
|
|
697
|
+
if (e instanceof ExecutableElement method
|
|
698
|
+
&& method.getKind() == ElementKind.METHOD
|
|
699
|
+
&& trees.getTree(e) != null
|
|
700
|
+
&& overridden(method) != null) {
|
|
701
|
+
return method;
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
private void documentation(Xml node, Element e) {
|
|
707
|
+
var doc = node.add("doc");
|
|
708
|
+
var own = parts(e);
|
|
709
|
+
var keys = new LinkedHashSet<>(own.keySet());
|
|
710
|
+
keys.add("body");
|
|
711
|
+
var method = overridingMethod(e);
|
|
712
|
+
if (method != null) {
|
|
713
|
+
for (int i = 0; i < method.getParameters().size(); i++) {
|
|
714
|
+
keys.add("param:" + i);
|
|
715
|
+
}
|
|
716
|
+
for (int i = 0; i < method.getTypeParameters().size(); i++) {
|
|
717
|
+
keys.add("typeparam:" + i);
|
|
718
|
+
}
|
|
719
|
+
if (method.getReturnType().getKind() != TypeKind.VOID) {
|
|
720
|
+
keys.add("return");
|
|
721
|
+
}
|
|
722
|
+
for (var type : method.getThrownTypes()) {
|
|
723
|
+
keys.add(THROWS + id(types.asElement(type)));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
for (var key : keys) {
|
|
727
|
+
Part part = own.get(key);
|
|
728
|
+
if (part == null && method != null) {
|
|
729
|
+
part = findPart(e, key, new HashSet<>());
|
|
730
|
+
}
|
|
731
|
+
if (part == null) {
|
|
732
|
+
if (method != null) {
|
|
733
|
+
warn(
|
|
734
|
+
e,
|
|
735
|
+
"Cannot inherit %s; provide the overridden method's source documentation or"
|
|
736
|
+
+ " document this part explicitly.",
|
|
737
|
+
key);
|
|
738
|
+
}
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
var xml = doc.add("part").set("kind", key.split(":", 2)[0]).set("label", label(e, key, part));
|
|
742
|
+
String context = key.startsWith(THROWS) ? THROWS + part.label() : key;
|
|
743
|
+
emit(xml, part.owner(), context, part.trees(), new HashSet<>());
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Labels follow the documented declaration, even when the prose is inherited. */
|
|
748
|
+
private static String label(Element e, String key, Part part) {
|
|
749
|
+
if (key.startsWith(THROWS)) {
|
|
750
|
+
return key.substring(THROWS.length()).replaceFirst(".*#", "");
|
|
751
|
+
}
|
|
752
|
+
boolean typeParameter = key.startsWith("typeparam:");
|
|
753
|
+
if (!(e instanceof ExecutableElement method) || !(typeParameter || key.startsWith("param:"))) {
|
|
754
|
+
return part.label();
|
|
755
|
+
}
|
|
756
|
+
int index = Integer.parseInt(key.split(":")[1]);
|
|
757
|
+
var declared = typeParameter ? method.getTypeParameters() : method.getParameters();
|
|
758
|
+
String name =
|
|
759
|
+
index >= 0 && index < declared.size()
|
|
760
|
+
? declared.get(index).getSimpleName().toString()
|
|
761
|
+
: part.label();
|
|
762
|
+
return typeParameter ? "<" + name + ">" : name;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Comment content
|
|
766
|
+
|
|
767
|
+
private Element resolve(Element owner, DocTree reference) {
|
|
768
|
+
var comment = trees.getDocCommentTree(owner);
|
|
769
|
+
var path = trees.getPath(owner);
|
|
770
|
+
if (comment == null || path == null) {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
var docPath = DocTreePath.getPath(new DocTreePath(path, comment), reference);
|
|
774
|
+
return docPath == null ? null : trees.getElement(docPath);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private void emit(
|
|
778
|
+
Xml parent, Element owner, String key, List<? extends DocTree> content, Set<String> chain) {
|
|
779
|
+
for (var tree : content) {
|
|
780
|
+
switch (tree) {
|
|
781
|
+
// ErroneousTree extends TextTree, so it must come first.
|
|
782
|
+
case ErroneousTree error -> {
|
|
783
|
+
warn(owner, "Malformed Javadoc: %s", error.getDiagnostic().getMessage(Locale.ROOT));
|
|
784
|
+
parent.addText("literal", error.getBody());
|
|
785
|
+
}
|
|
786
|
+
case TextTree text -> parent.addText("text", text.getBody());
|
|
787
|
+
case LiteralTree literal -> {
|
|
788
|
+
String tag = tree.getKind() == DocTree.Kind.CODE ? "code" : "literal";
|
|
789
|
+
parent.addText(tag, literal.getBody().getBody());
|
|
790
|
+
}
|
|
791
|
+
case SnippetTree snippet -> snippet(parent, owner, snippet);
|
|
792
|
+
case ValueTree value -> value(parent, owner, value);
|
|
793
|
+
case EntityTree entity -> parent.addText("text", characters(entity));
|
|
794
|
+
case LinkTree link -> {
|
|
795
|
+
boolean plain = tree.getKind() == DocTree.Kind.LINK_PLAIN;
|
|
796
|
+
reference(parent, owner, link.getReference(), link.getLabel(), plain);
|
|
797
|
+
}
|
|
798
|
+
case ReferenceTree ref -> reference(parent, owner, ref, List.of(), false);
|
|
799
|
+
case StartElementTree start -> startElement(parent, start);
|
|
800
|
+
case EndElementTree end -> parent.add("end").set("name", end.getName().toString());
|
|
801
|
+
case InheritDocTree inherit -> inheritDoc(parent, owner, key, inherit, chain);
|
|
802
|
+
case ReturnTree ret -> {
|
|
803
|
+
parent.addText("text", "Returns ");
|
|
804
|
+
emit(parent, owner, "return", ret.getDescription(), chain);
|
|
805
|
+
parent.addText("text", ".");
|
|
806
|
+
}
|
|
807
|
+
default -> {
|
|
808
|
+
if (tree.getKind() != DocTree.Kind.COMMENT) {
|
|
809
|
+
warn(
|
|
810
|
+
owner,
|
|
811
|
+
"Unsupported Javadoc construct %s; use traditional text or <pre>{@code ...}</pre>"
|
|
812
|
+
+ " (Markdown expansion is deferred).",
|
|
813
|
+
tree.getKind());
|
|
814
|
+
parent.addText("literal", tree.toString());
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
private String characters(EntityTree entity) {
|
|
822
|
+
return Objects.requireNonNullElse(trees.getCharacters(entity), entity.toString());
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private void startElement(Xml parent, StartElementTree start) {
|
|
826
|
+
var node =
|
|
827
|
+
parent
|
|
828
|
+
.add("start")
|
|
829
|
+
.set("name", start.getName().toString())
|
|
830
|
+
.set("closed", Boolean.toString(start.isSelfClosing()));
|
|
831
|
+
for (var attribute : start.getAttributes()) {
|
|
832
|
+
if (attribute instanceof AttributeTree a) {
|
|
833
|
+
node.addAttr(a.getName().toString(), attributeValue(a));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Returns the decoded attribute value; valueless attributes such as {@code compact} are empty.
|
|
840
|
+
*/
|
|
841
|
+
private String attributeValue(AttributeTree attribute) {
|
|
842
|
+
if (attribute.getValueKind() == AttributeTree.ValueKind.EMPTY) {
|
|
843
|
+
return "";
|
|
844
|
+
}
|
|
845
|
+
return attribute.getValue().stream()
|
|
846
|
+
.map(v -> v instanceof EntityTree entity ? characters(entity) : v.toString())
|
|
847
|
+
.collect(Collectors.joining());
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
private void reference(
|
|
851
|
+
Xml parent, Element owner, ReferenceTree ref, List<? extends DocTree> label, boolean plain) {
|
|
852
|
+
if (ref == null) {
|
|
853
|
+
warn(owner, "Malformed Javadoc: link is missing a reference target.");
|
|
854
|
+
parent.addText("literal", "[missing link target]");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
var link = parent.add("link").set("plain", Boolean.toString(plain));
|
|
858
|
+
var target = resolve(owner, ref);
|
|
859
|
+
link.set("target", target == null ? ref.toString() : id(target));
|
|
860
|
+
if (target == null) {
|
|
861
|
+
warn(
|
|
862
|
+
owner,
|
|
863
|
+
"Unresolved Javadoc reference %s; check spelling, source paths and classpath.",
|
|
864
|
+
ref);
|
|
865
|
+
} else {
|
|
866
|
+
String role =
|
|
867
|
+
switch (target) {
|
|
868
|
+
case TypeElement _ -> "type";
|
|
869
|
+
case ExecutableElement _ -> "method";
|
|
870
|
+
case PackageElement _ -> "package";
|
|
871
|
+
default -> "field";
|
|
872
|
+
};
|
|
873
|
+
link.set("role", role);
|
|
874
|
+
javadocLocation(link, target);
|
|
875
|
+
}
|
|
876
|
+
if (label.isEmpty()) {
|
|
877
|
+
link.addText("text", ref.toString());
|
|
878
|
+
} else {
|
|
879
|
+
emit(link, owner, "label", label, new HashSet<>());
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** Records where standard Javadoc publishes the target, for external Javadoc links. */
|
|
884
|
+
private void javadocLocation(Xml link, Element target) {
|
|
885
|
+
Element type = target;
|
|
886
|
+
while (type != null && !(type instanceof TypeElement) && !(type instanceof PackageElement)) {
|
|
887
|
+
type = type.getEnclosingElement();
|
|
888
|
+
}
|
|
889
|
+
if (type == null) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
String pkg = elements.getPackageOf(type).getQualifiedName().toString();
|
|
893
|
+
String path = pkg.isEmpty() ? "" : pkg.replace('.', '/') + "/";
|
|
894
|
+
if (type instanceof TypeElement t) {
|
|
895
|
+
String name = t.getQualifiedName().toString();
|
|
896
|
+
path += name.substring(pkg.isEmpty() ? 0 : pkg.length() + 1) + ".html";
|
|
897
|
+
} else {
|
|
898
|
+
path += "package-summary.html";
|
|
899
|
+
}
|
|
900
|
+
var module = elements.getModuleOf(type);
|
|
901
|
+
String moduleName =
|
|
902
|
+
module == null || module.isUnnamed() ? "" : module.getQualifiedName().toString();
|
|
903
|
+
String fragment =
|
|
904
|
+
switch (target) {
|
|
905
|
+
case ExecutableElement method -> methodFragment(method);
|
|
906
|
+
case VariableElement variable -> variable.getSimpleName().toString();
|
|
907
|
+
default -> "";
|
|
908
|
+
};
|
|
909
|
+
link.set("path", path).set("module", moduleName).set("fragment", fragment);
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Writes a constant's Java source form, as the standard doclet does. A referenced constant links
|
|
914
|
+
* to its field; {@code {@value}} in the constant's own comment is plain text.
|
|
915
|
+
*/
|
|
916
|
+
private void value(Xml parent, Element owner, ValueTree value) {
|
|
917
|
+
var ref = value.getReference();
|
|
918
|
+
Element target = ref == null ? owner : resolve(owner, ref);
|
|
919
|
+
Object constant = target instanceof VariableElement field ? field.getConstantValue() : null;
|
|
920
|
+
if (constant == null) {
|
|
921
|
+
if (ref == null) {
|
|
922
|
+
warn(owner, "{@value} without a reference must document a constant field.");
|
|
923
|
+
} else if (target == null) {
|
|
924
|
+
warn(
|
|
925
|
+
owner,
|
|
926
|
+
"Unresolved Javadoc reference %s; check spelling, source paths and classpath.",
|
|
927
|
+
ref);
|
|
928
|
+
} else {
|
|
929
|
+
warn(owner, "{@value %s} must reference a constant field.", ref);
|
|
930
|
+
}
|
|
931
|
+
parent.addText("literal", value.toString());
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
String text = elements.getConstantExpression(constant);
|
|
935
|
+
if (value.getFormat() != null) {
|
|
936
|
+
String format = value.getFormat().getBody();
|
|
937
|
+
try {
|
|
938
|
+
text = String.format(Locale.ROOT, format, constant);
|
|
939
|
+
} catch (IllegalFormatException ex) {
|
|
940
|
+
warn(
|
|
941
|
+
owner,
|
|
942
|
+
"Invalid {@value} format %s for %s; use a java.util.Formatter format.",
|
|
943
|
+
format,
|
|
944
|
+
constant);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
if (ref == null) {
|
|
948
|
+
parent.addText("literal", text);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
var link =
|
|
952
|
+
parent.add("link").set("plain", "true").set("target", id(target)).set("role", "field");
|
|
953
|
+
javadocLocation(link, target);
|
|
954
|
+
link.addText("literal", text);
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private static String methodFragment(ExecutableElement method) {
|
|
958
|
+
String fragment =
|
|
959
|
+
method.getSimpleName()
|
|
960
|
+
+ method.getParameters().stream()
|
|
961
|
+
.map(p -> linkType(p.asType()))
|
|
962
|
+
.collect(Collectors.joining(",", "(", ")"));
|
|
963
|
+
return method.isVarArgs() ? fragment.substring(0, fragment.length() - 3) + "...)" : fragment;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
private void snippet(Xml parent, Element owner, SnippetTree snippet) {
|
|
967
|
+
String language = "java";
|
|
968
|
+
boolean supported = snippet.getBody() != null && snippet.getAttributes().size() <= 1;
|
|
969
|
+
for (var attribute : snippet.getAttributes()) {
|
|
970
|
+
if (attribute instanceof AttributeTree a && a.getName().contentEquals("lang")) {
|
|
971
|
+
language = attributeValue(a);
|
|
972
|
+
supported &= language.matches("[A-Za-z0-9_+.-]+");
|
|
973
|
+
} else {
|
|
974
|
+
supported = false;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
String body = snippet.getBody() == null ? "" : snippet.getBody().getBody();
|
|
978
|
+
supported &= !SNIPPET_MARKUP.matcher(body).matches();
|
|
979
|
+
if (!supported) {
|
|
980
|
+
warn(
|
|
981
|
+
owner,
|
|
982
|
+
"Unsupported snippet attributes or markup; use an inline body with optional lang,"
|
|
983
|
+
+ " without files, regions, IDs or markup actions.");
|
|
984
|
+
parent.addText("literal", snippet.toString());
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
parent.add("start").set("name", "pre").addAttr("lang", language);
|
|
988
|
+
parent.addText("literal", body.stripIndent());
|
|
989
|
+
parent.add("end").set("name", "pre");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private void inheritDoc(
|
|
993
|
+
Xml parent, Element owner, String key, InheritDocTree inherit, Set<String> chain) {
|
|
994
|
+
boolean inheritable =
|
|
995
|
+
key.equals("body")
|
|
996
|
+
|| key.equals("return")
|
|
997
|
+
|| key.startsWith("param:")
|
|
998
|
+
|| key.startsWith("typeparam:")
|
|
999
|
+
|| key.startsWith(THROWS);
|
|
1000
|
+
if (!(owner instanceof ExecutableElement method)
|
|
1001
|
+
|| method.getKind() != ElementKind.METHOD
|
|
1002
|
+
|| !inheritable) {
|
|
1003
|
+
warn(
|
|
1004
|
+
owner,
|
|
1005
|
+
"inheritDoc is only valid in overriding method descriptions, parameters, returns and"
|
|
1006
|
+
+ " throws.");
|
|
1007
|
+
parent.addText("literal", "[unresolved inheritDoc]");
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
ExecutableElement ancestor = overridden(method);
|
|
1011
|
+
if (inherit.getSupertype() != null) {
|
|
1012
|
+
var owningType = (TypeElement) method.getEnclosingElement();
|
|
1013
|
+
ancestor =
|
|
1014
|
+
resolve(owner, inherit.getSupertype()) instanceof TypeElement supertype
|
|
1015
|
+
? search(method, owningType, supertype, new HashSet<>())
|
|
1016
|
+
: null;
|
|
1017
|
+
}
|
|
1018
|
+
String step = id(owner) + ":" + key;
|
|
1019
|
+
Part inherited =
|
|
1020
|
+
ancestor == null
|
|
1021
|
+
? null
|
|
1022
|
+
: findPart(ancestor, inheritedKey(method, ancestor, key), new HashSet<>());
|
|
1023
|
+
if (inherited == null || !chain.add(step)) {
|
|
1024
|
+
warn(
|
|
1025
|
+
owner,
|
|
1026
|
+
"Cannot resolve inheritDoc for %s; provide ancestor source documentation or write this"
|
|
1027
|
+
+ " part explicitly.",
|
|
1028
|
+
key);
|
|
1029
|
+
parent.addText("literal", "[unresolved inheritDoc]");
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
String context = key.startsWith(THROWS) ? THROWS + inherited.label() : key;
|
|
1033
|
+
emit(parent, inherited.owner(), context, inherited.trees(), chain);
|
|
1034
|
+
chain.remove(step);
|
|
1035
|
+
}
|
|
1036
|
+
}
|