mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-05 02:39:44 +02:00
Python3 support
This commit is contained in:
parent
d7c1f65f43
commit
92d0f1dacf
101 changed files with 11413 additions and 13 deletions
|
@ -0,0 +1,156 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.util.Elements;
|
||||
import javax.tools.Diagnostic;
|
||||
|
||||
import com.sun.source.doctree.AttributeTree;
|
||||
import com.sun.source.doctree.DocCommentTree;
|
||||
import com.sun.source.doctree.DocTree;
|
||||
import com.sun.source.util.DocTreePath;
|
||||
import com.sun.source.util.DocTrees;
|
||||
import com.sun.source.util.TreePath;
|
||||
|
||||
import jdk.javadoc.doclet.DocletEnvironment;
|
||||
import jdk.javadoc.doclet.Reporter;
|
||||
|
||||
/**
|
||||
* Base class for recursively converting documentation
|
||||
*/
|
||||
abstract class DocConverter {
|
||||
|
||||
static final int INDENT_WIDTH = 4;
|
||||
|
||||
private final DocletEnvironment env;
|
||||
private final Reporter log;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DocConverter}
|
||||
*
|
||||
* @param env the doclet environment
|
||||
* @param log the log
|
||||
*/
|
||||
DocConverter(DocletEnvironment env, Reporter log) {
|
||||
this.env = env;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the Javadoc tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
abstract String convertTag(Element el, DocTree tag, ListIterator<? extends DocTree> it);
|
||||
|
||||
/**
|
||||
* Converts the provided doc tree
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tree the doc tree
|
||||
* @return the converted doc tree
|
||||
*/
|
||||
public String convertTree(Element el, List<? extends DocTree> tree) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
ListIterator<? extends DocTree> it = tree.listIterator();
|
||||
while (it.hasNext()) {
|
||||
builder.append(convertTag(el, it.next(), it));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning with the provided message
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the current tag
|
||||
* @param message the message
|
||||
*/
|
||||
final void logWarning(Element el, DocTree tag, String message) {
|
||||
try {
|
||||
DocCommentTree tree = env.getDocTrees().getDocCommentTree(el);
|
||||
TreePath treePath = env.getDocTrees().getPath(el);
|
||||
DocTreePath path = DocTreePath.getPath(treePath, tree, tag);
|
||||
if (path != null) {
|
||||
log.print(Diagnostic.Kind.WARNING, path, message);
|
||||
}
|
||||
else {
|
||||
log.print(Diagnostic.Kind.WARNING, el, message);
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs an error with the provided message
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the current tag
|
||||
* @param message the message
|
||||
*/
|
||||
final void logError(Element el, DocTree tag, String message) {
|
||||
try {
|
||||
DocCommentTree tree = env.getDocTrees().getDocCommentTree(el);
|
||||
TreePath treePath = env.getDocTrees().getPath(el);
|
||||
DocTreePath path = DocTreePath.getPath(treePath, tree, tag);
|
||||
if (path != null) {
|
||||
log.print(Diagnostic.Kind.ERROR, path, message);
|
||||
}
|
||||
else {
|
||||
log.print(Diagnostic.Kind.ERROR, el, message);
|
||||
}
|
||||
}
|
||||
catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
final DocTrees getDocTrees() {
|
||||
return env.getDocTrees();
|
||||
}
|
||||
|
||||
final Elements getElementUtils() {
|
||||
return env.getElementUtils();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a mapping of the provided list of attributes
|
||||
*
|
||||
* @param attributes the attributes list
|
||||
* @return the attributes mapping
|
||||
*/
|
||||
Map<String, String> getAttributes(Element el, List<? extends DocTree> attributes) {
|
||||
return attributes
|
||||
.stream()
|
||||
.filter(AttributeTree.class::isInstance)
|
||||
.map(AttributeTree.class::cast)
|
||||
.collect(Collectors.toMap(attr -> attr.getName().toString().toLowerCase(),
|
||||
attr -> attr.getValue() != null ? convertTree(el, attr.getValue()) : ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the lines in the provided text to the same indentation level
|
||||
*
|
||||
* @param text the text
|
||||
* @return the new text all aligned to the same indentation level
|
||||
*/
|
||||
static String alignIndent(String text) {
|
||||
int index = text.indexOf('\n');
|
||||
if (index == -1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
StringBuilder builder = new StringBuilder();
|
||||
return builder.append(text.substring(0, index + 1))
|
||||
.append(text.substring(index + 1).stripIndent())
|
||||
.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.lang.model.element.PackageElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.util.Elements;
|
||||
|
||||
/**
|
||||
* A builder class for the pseudo ghidra.ghidra_builtins package
|
||||
*/
|
||||
class GhidraBuiltinsBuilder {
|
||||
|
||||
private static final String INDENT = "";
|
||||
|
||||
private final PythonTypeStubDoclet doclet;
|
||||
private final PythonTypeStubType api;
|
||||
private final PythonTypeStubType script;
|
||||
|
||||
/**
|
||||
* Creates a new {@link GhidraBuiltinsBuilder}
|
||||
*
|
||||
* @param doclet the current doclet
|
||||
*/
|
||||
GhidraBuiltinsBuilder(PythonTypeStubDoclet doclet) {
|
||||
this.doclet = doclet;
|
||||
this.api = getType(doclet, "ghidra.program.flatapi.FlatProgramAPI");
|
||||
this.script = getType(doclet, "ghidra.app.script.GhidraScript");
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pseudo ghidra.ghidra_builtins package
|
||||
*/
|
||||
void process() {
|
||||
File root = new File(doclet.getDestDir(), "ghidra-stubs/ghidra_builtins");
|
||||
root.mkdirs();
|
||||
File stub = new File(root, "__init__.pyi");
|
||||
try (PrintWriter printer = new PrintWriter(new FileWriter(stub))) {
|
||||
process(printer);
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pseudo ghidra.ghidra_builtins package using the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
*/
|
||||
private void process(PrintWriter printer) {
|
||||
// collect methods and fields early to ensure protected visibility
|
||||
api.getMethods(true, true);
|
||||
script.getMethods(true, true);
|
||||
api.getFields(true);
|
||||
script.getFields(true);
|
||||
|
||||
script.writeJavaDoc(printer, INDENT);
|
||||
printer.println();
|
||||
|
||||
printScriptImports(printer);
|
||||
printTypeVars(printer);
|
||||
|
||||
// we need to keep track of things to export for __all__
|
||||
Set<String> exports = new LinkedHashSet<>();
|
||||
|
||||
printFields(printer, exports);
|
||||
|
||||
printer.println();
|
||||
printer.println();
|
||||
|
||||
printMethods(printer, exports);
|
||||
|
||||
printer.print("__all__ = [");
|
||||
printer.print(String.join(", ", exports));
|
||||
printer.println("]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints all necessary TypeVars
|
||||
*
|
||||
* @param printer the printer
|
||||
*/
|
||||
private void printTypeVars(PrintWriter printer) {
|
||||
for (String typevar : getScriptTypeVars()) {
|
||||
printer.print(typevar);
|
||||
printer.print(" = typing.TypeVar(\"");
|
||||
printer.print(typevar);
|
||||
printer.println("\")");
|
||||
}
|
||||
printer.println();
|
||||
printer.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints all the script fields
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param exports the set of fields to export
|
||||
*/
|
||||
private void printFields(PrintWriter printer, Set<String> exports) {
|
||||
// always use false for static so typing.ClassVar is not emitted
|
||||
for (VariableElement field : api.getFields(true)) {
|
||||
api.printField(field, printer, INDENT, false);
|
||||
exports.add('"' + field.getSimpleName().toString() + '"');
|
||||
}
|
||||
for (VariableElement field : script.getFields(true)) {
|
||||
script.printField(field, printer, INDENT, false);
|
||||
exports.add('"' + field.getSimpleName().toString() + '"');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints all the script methods
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param exports the set of methods to export
|
||||
*/
|
||||
private void printMethods(PrintWriter printer, Set<String> exports) {
|
||||
// methods must be sorted by name for typing.overload
|
||||
List<PythonTypeStubMethod> apiMethods = api.getMethods(true, true);
|
||||
List<PythonTypeStubMethod> scriptMethods = script.getMethods(true, true);
|
||||
|
||||
int length = apiMethods.size() + scriptMethods.size();
|
||||
List<PythonTypeStubMethod> methods = new ArrayList<>(length);
|
||||
|
||||
methods.addAll(apiMethods);
|
||||
methods.addAll(scriptMethods);
|
||||
methods.sort(null);
|
||||
|
||||
ListIterator<PythonTypeStubMethod> methodIterator = methods.listIterator();
|
||||
|
||||
while (methodIterator.hasNext()) {
|
||||
PythonTypeStubMethod method = methodIterator.next();
|
||||
boolean overload = PythonTypeStubType.isOverload(methods, methodIterator, method);
|
||||
method.process(printer, INDENT, overload);
|
||||
exports.add('"' + method.getName() + '"');
|
||||
printer.println();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all imported packages
|
||||
*
|
||||
* @return the list of packages
|
||||
*/
|
||||
private List<PackageElement> getScriptPackages() {
|
||||
Set<PackageElement> packages = new HashSet<>();
|
||||
for (TypeElement type : api.getImportedTypes()) {
|
||||
packages.add(PythonTypeStubElement.getPackage(type));
|
||||
}
|
||||
for (TypeElement type : script.getImportedTypes()) {
|
||||
packages.add(PythonTypeStubElement.getPackage(type));
|
||||
}
|
||||
List<PackageElement> res = new ArrayList<>(packages);
|
||||
res.sort(PythonTypeStubElement::compareQualifiedNameable);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the imports needed by this package
|
||||
*
|
||||
* @param printer the printer
|
||||
*/
|
||||
private void printScriptImports(PrintWriter printer) {
|
||||
printer.println("import collections.abc");
|
||||
printer.println("import typing");
|
||||
printer.println("from warnings import deprecated # type: ignore");
|
||||
printer.println();
|
||||
printer.println("import jpype # type: ignore");
|
||||
printer.println("import jpype.protocol # type: ignore");
|
||||
printer.println();
|
||||
doclet.printImports(printer, getScriptPackages());
|
||||
printer.println();
|
||||
printer.println();
|
||||
printer.println("from ghidra.app.script import *");
|
||||
printer.println();
|
||||
printer.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of TypeVars needed by this package
|
||||
*
|
||||
* @return the list of TypeVars
|
||||
*/
|
||||
private List<String> getScriptTypeVars() {
|
||||
// all this for only two typing.TypeVar
|
||||
// at least this is future proof
|
||||
Set<String> vars = new HashSet<>(api.getTypeVars());
|
||||
vars.addAll(script.getTypeVars());
|
||||
|
||||
List<String> res = new ArrayList<>(vars);
|
||||
res.sort(null);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PythonTypeStubType for the provided type name
|
||||
*
|
||||
* @param doclet the current doclet
|
||||
* @param name the type name
|
||||
* @return the requested type
|
||||
*/
|
||||
private static PythonTypeStubType getType(PythonTypeStubDoclet doclet, String name) {
|
||||
Elements elements = doclet.getElementUtils();
|
||||
TypeElement type = elements.getTypeElement(name);
|
||||
PackageElement pkg = (PackageElement) type.getEnclosingElement();
|
||||
return new PythonTypeStubType(new PythonTypeStubPackage(doclet, pkg), type);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,494 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
|
||||
import com.sun.source.doctree.DocTree;
|
||||
import com.sun.source.doctree.EndElementTree;
|
||||
import com.sun.source.doctree.LinkTree;
|
||||
import com.sun.source.doctree.StartElementTree;
|
||||
import com.sun.source.doctree.TextTree;
|
||||
|
||||
import jdk.javadoc.doclet.DocletEnvironment;
|
||||
import jdk.javadoc.doclet.Reporter;
|
||||
|
||||
/**
|
||||
* Helper class for converting HTML to reStructuredText
|
||||
*/
|
||||
public final class HtmlConverter extends DocConverter {
|
||||
|
||||
private final JavadocConverter docConverter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link HtmlConverter}
|
||||
*
|
||||
* @param env the doclet environment
|
||||
* @param log the log
|
||||
*/
|
||||
public HtmlConverter(DocletEnvironment env, Reporter log, JavadocConverter docConverter) {
|
||||
super(env, log);
|
||||
this.docConverter = docConverter;
|
||||
}
|
||||
|
||||
@Override
|
||||
String convertTag(Element el, DocTree tag, ListIterator<? extends DocTree> it) {
|
||||
return docConverter.convertTag(el, tag, it);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a map of the attributes in the html element
|
||||
*
|
||||
* @param start the start element
|
||||
* @return the attributes map
|
||||
*/
|
||||
public Map<String, String> getAttributes(Element el, StartElementTree start) {
|
||||
return getAttributes(el, start.getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a warning about an unterminated html tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the current tag
|
||||
*/
|
||||
public void logUnterminatedHtml(Element el, StartElementTree tag) {
|
||||
try {
|
||||
logWarning(el, tag, "unterminated html tag");
|
||||
}
|
||||
catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided HTML to reStructuredText where possible
|
||||
*
|
||||
* @param tag the html
|
||||
* @param el the element containing the html
|
||||
* @param it the Javadoc tree iterator
|
||||
* @return the converted string
|
||||
*/
|
||||
String convertHtml(HtmlDocTree tag, Element el, ListIterator<? extends DocTree> it) {
|
||||
StartElementTree start = tag.getStartTag();
|
||||
return switch (tag.getHtmlKind()) {
|
||||
case A -> convertAnchor(tag, el);
|
||||
case B -> "**" + convertTree(el, tag.getBody()) + "**";
|
||||
case BIG -> ""; // not in rst
|
||||
case BLOCKQUOTE -> convertBlockQuote(tag, el);
|
||||
case BR -> "\n";
|
||||
case CAPTION -> {
|
||||
logError(el, start, "<caption> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case CITE -> "*" + convertTree(el, tag.getBody()) + "*";
|
||||
case CODE -> "``" + convertTree(el, tag.getBody()) + "``";
|
||||
case DD -> {
|
||||
logError(el, start, "<dd> outside of list");
|
||||
yield start.toString();
|
||||
}
|
||||
case DEL -> "~~" + convertTree(el, tag.getBody()) + "~~";
|
||||
// rarely used, not bothering with id attribute
|
||||
case DFN -> "*" + convertTree(el, tag.getBody()) + "*";
|
||||
case DIV -> convertTree(el, tag.getBody()); // do nothing
|
||||
case DL -> convertDescriptionList(tag, el);
|
||||
case DT -> {
|
||||
logError(el, start, "<dt> outside of list");
|
||||
yield start.toString();
|
||||
}
|
||||
case EM -> "*" + convertTree(el, tag.getBody()) + "*";
|
||||
case H1 -> convertHeader(tag, el, '#');
|
||||
case H2 -> convertHeader(tag, el, '*');
|
||||
case H3 -> convertHeader(tag, el, '=');
|
||||
case H4 -> convertHeader(tag, el, '-');
|
||||
case H5 -> convertHeader(tag, el, '^');
|
||||
case H6 -> convertHeader(tag, el, '\'');
|
||||
case HR -> "---\n";
|
||||
case I -> "*" + convertTree(el, tag.getBody()) + "*";
|
||||
case IMG -> ""; // not supported because the images wouldn't be available
|
||||
case INS -> convertTree(el, tag.getBody()); // no underline in rst
|
||||
case LI -> {
|
||||
logError(el, start, "<li> outside of list");
|
||||
yield start.toString();
|
||||
}
|
||||
case OL -> convertOrderedList(tag, el);
|
||||
case P -> "\n";
|
||||
case PRE -> convertTree(el, tag.getBody()); // do nothing
|
||||
case SMALL -> ""; // not in rst
|
||||
case SPAN -> convertTree(el, tag.getBody()); // no colored text in rst
|
||||
case STRONG -> "**" + convertTree(el, tag.getBody()) + "**";
|
||||
case SUB -> ""; // no subscript in rst
|
||||
case SUP -> ""; // no superscript in rst
|
||||
case TABLE -> convertTable(tag, el);
|
||||
case TBODY -> {
|
||||
logError(el, start, "<tbody> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case TD -> {
|
||||
logError(el, start, "<td> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case TFOOT -> {
|
||||
logError(el, start, "<tfoot> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case TH -> {
|
||||
logError(el, start, "<th> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case THEAD -> {
|
||||
logError(el, start, "<thead> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case TR -> {
|
||||
logError(el, start, "<tr> outside of table");
|
||||
yield start.toString();
|
||||
}
|
||||
case TT -> "``" + convertTree(el, tag.getBody()) + "``";
|
||||
case U -> convertTree(el, tag.getBody()); // no underline in rst
|
||||
case UL -> convertUnorderedList(tag, el);
|
||||
case UNSUPPORTED -> {
|
||||
logWarning(el, start, "unsupported html tag");
|
||||
yield start.toString();
|
||||
}
|
||||
case VAR -> "*" + convertTree(el, tag.getBody()) + "*";
|
||||
};
|
||||
}
|
||||
|
||||
String convertHtml(StartElementTree start, Element el, ListIterator<? extends DocTree> it) {
|
||||
HtmlDocTree tag = HtmlDocTree.getTree(this, start, el, it);
|
||||
return convertHtml(tag, el, it);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@literal <blockquote>} tag
|
||||
*
|
||||
* @param html the html
|
||||
* @param el the element
|
||||
* @return the converted blockquote
|
||||
*/
|
||||
private String convertBlockQuote(HtmlDocTree html, Element el) {
|
||||
String body = convertTree(el, html.getBody());
|
||||
return body.indent(INDENT_WIDTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@literal <H1>} ... {@literal <H6>} tags
|
||||
*
|
||||
* @param html the html
|
||||
* @param el the element
|
||||
* @param header the header character
|
||||
* @return the converted header
|
||||
*/
|
||||
private String convertHeader(HtmlDocTree html, Element el, char header) {
|
||||
String body = convertTree(el, html.getBody());
|
||||
int length = body.length();
|
||||
StringBuilder builder = new StringBuilder();
|
||||
return builder.append('\n')
|
||||
.repeat(header, length)
|
||||
.append('\n')
|
||||
.append(body)
|
||||
.append('\n')
|
||||
.repeat(header, length)
|
||||
.append('\n')
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a {@literal <li>} tag
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted list entry
|
||||
*/
|
||||
private String convertListEntry(HtmlDocTree tree, Element el) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (tag instanceof HtmlDocTree html) {
|
||||
switch (html.getHtmlKind()) {
|
||||
case OL: {
|
||||
String list = convertOrderedList(html, el);
|
||||
builder.append(list.indent(INDENT_WIDTH));
|
||||
break;
|
||||
}
|
||||
case UL: {
|
||||
String list = convertUnorderedList(html, el);
|
||||
builder.append(list.indent(INDENT_WIDTH));
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
builder.append(convertTree(el, html.getBody()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
String entry = docConverter.convertTag(el, tag, null);
|
||||
builder.append(alignIndent(entry));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a description list {@literal <dl>}
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted list
|
||||
*/
|
||||
private String convertDescriptionList(HtmlDocTree tree, Element el) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('\n');
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (tag instanceof HtmlDocTree html) {
|
||||
if (html.getHtmlKind() == HtmlTagKind.DT) {
|
||||
builder.append(convertTree(el, html.getBody()));
|
||||
}
|
||||
else if (html.getHtmlKind() == HtmlTagKind.DD) {
|
||||
String body = convertTree(el, html.getBody());
|
||||
builder.append(body.indent(INDENT_WIDTH))
|
||||
.append('\n');
|
||||
}
|
||||
else {
|
||||
builder.append(convertTree(el, html.getBody()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(docConverter.convertTag(el, tag, null));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an ordered list {@literal <ol>}
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted list
|
||||
*/
|
||||
private String convertOrderedList(HtmlDocTree tree, Element el) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int num = 1; // because #. doesn't always work like it should
|
||||
builder.append('\n');
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (tag instanceof HtmlDocTree html) {
|
||||
if (html.getHtmlKind() == HtmlTagKind.LI) {
|
||||
builder.append(num++)
|
||||
.append(". ")
|
||||
.append(convertListEntry(html, el))
|
||||
.append('\n');
|
||||
}
|
||||
else {
|
||||
builder.append(convertTree(el, html.getBody()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(docConverter.convertTag(el, tag, null));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an unordered list {@literal <ul>}
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted list
|
||||
*/
|
||||
private String convertUnorderedList(HtmlDocTree tree, Element el) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('\n');
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (tag instanceof HtmlDocTree html) {
|
||||
if (html.getHtmlKind() == HtmlTagKind.LI) {
|
||||
builder.append("* ")
|
||||
.append(convertListEntry(html, el))
|
||||
.append('\n');
|
||||
}
|
||||
else {
|
||||
builder.append(convertTree(el, html.getBody()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
builder.append(docConverter.convertTag(el, tag, null));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an anchor {@literal <a id="#example">link text</a>}
|
||||
*
|
||||
* @param html the html
|
||||
* @param el the element
|
||||
* @return the converted html
|
||||
*/
|
||||
private String convertAnchor(HtmlDocTree html, Element el) {
|
||||
String label = convertTree(el, html.getBody()).stripLeading();
|
||||
Map<String, String> attrs = getAttributes(el, html.getStartTag());
|
||||
String id = attrs.get("id");
|
||||
if (id == null) {
|
||||
id = attrs.get("name");
|
||||
}
|
||||
if (id != null) {
|
||||
return "\n.. _" + id + ":\n\n" + label;
|
||||
}
|
||||
|
||||
String href = attrs.get("href");
|
||||
if (href == null) {
|
||||
logWarning(el, html.getStartTag(), "skipping anchor without an id or href");
|
||||
return "";
|
||||
}
|
||||
if (href.startsWith("#")) {
|
||||
// internal
|
||||
if (label.isBlank()) {
|
||||
return href.substring(1) + '_';
|
||||
}
|
||||
return '`' + label + " <" + href.substring(1) + "_>`_";
|
||||
}
|
||||
|
||||
// external
|
||||
if (label.isBlank()) {
|
||||
return '<' + href.substring(0) + '>';
|
||||
}
|
||||
return '`' + label + " <" + href + ">`_";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided tree to a raw html string
|
||||
*
|
||||
* @param el the element
|
||||
* @param tree the tree
|
||||
* @return the html string
|
||||
*/
|
||||
private String getRawHtml(Element el, List<? extends DocTree> tree) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (DocTree tag : tree) {
|
||||
switch (tag.getKind()) {
|
||||
case START_ELEMENT:
|
||||
case END_ELEMENT:
|
||||
builder.append(tag.toString());
|
||||
break;
|
||||
case OTHER:
|
||||
if (!(tag instanceof HtmlDocTree)) {
|
||||
logError(el, tag, "Unexpected OTHER tag kind");
|
||||
return "";
|
||||
}
|
||||
HtmlDocTree html = (HtmlDocTree) tag;
|
||||
builder.append(html.getStartTag().toString())
|
||||
.append(getRawHtml(el, html.getBody()));
|
||||
EndElementTree end = html.getEndTag();
|
||||
if (end != null) {
|
||||
builder.append(end.toString());
|
||||
}
|
||||
break;
|
||||
case LINK:
|
||||
case LINK_PLAIN:
|
||||
builder.append(getRawHtml(el, ((LinkTree) tag).getLabel()));
|
||||
break;
|
||||
default:
|
||||
builder.append(docConverter.convertTag(el, tag, null));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the html tree to a raw html string
|
||||
*
|
||||
* @param html the html tree
|
||||
* @param el the element
|
||||
* @return the html
|
||||
*/
|
||||
private String getRawHtml(HtmlDocTree html, Element el) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(html.getStartTag().toString())
|
||||
.append(getRawHtml(el, html.getBody()));
|
||||
EndElementTree end = html.getEndTag();
|
||||
if (end != null) {
|
||||
builder.append(end.toString());
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a table {@literal <table>} to reStructuredText if possible
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted table or original html if not convertible
|
||||
*/
|
||||
private String convertTable(HtmlDocTree tree, Element el) {
|
||||
try {
|
||||
return tryConvertTable(tree, el);
|
||||
}
|
||||
catch (UnsupportedOperationException e) {
|
||||
// use raw html directive
|
||||
// this may not be supported by all IDEs but it is better then nothing
|
||||
// if your IDE doesn't support it, try tilting your head and squinting
|
||||
StringBuilder builder = new StringBuilder();
|
||||
return builder.append("\n\n.. raw:: html\n\n")
|
||||
.append(getRawHtml(tree, el).indent(INDENT_WIDTH))
|
||||
.append('\n')
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a table {@literal <table>}
|
||||
*
|
||||
* @param tree the html
|
||||
* @param el the element
|
||||
* @return the converted table
|
||||
* @throws UnsupportedOperationException if the table contains nested rows
|
||||
*/
|
||||
private String tryConvertTable(HtmlDocTree tree, Element el) {
|
||||
RstTableBuilder tbl = new RstTableBuilder(this, el);
|
||||
ListIterator<? extends DocTree> it = tree.getBody().listIterator();
|
||||
while (it.hasNext()) {
|
||||
DocTree tag = it.next();
|
||||
switch (tag.getKind()) {
|
||||
case OTHER:
|
||||
if (!(tag instanceof HtmlDocTree)) {
|
||||
logError(el, tag, "Unexpected OTHER tag kind");
|
||||
return "";
|
||||
}
|
||||
HtmlDocTree html = (HtmlDocTree) tag;
|
||||
switch (html.getHtmlKind()) {
|
||||
case TBODY:
|
||||
case TFOOT:
|
||||
case THEAD:
|
||||
tbl.addRowGroup(html);
|
||||
break;
|
||||
case TR:
|
||||
tbl.addRow(html);
|
||||
break;
|
||||
case CAPTION:
|
||||
tbl.addCaption(convertTree(el, html.getBody()));
|
||||
break;
|
||||
default:
|
||||
logError(el, tag,
|
||||
"unexpected html tag encountered while parsing table");
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case TEXT:
|
||||
String body = ((TextTree) tag).getBody();
|
||||
if (!body.isBlank()) {
|
||||
logWarning(el, tag, "skipping unexpected text in table");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logError(el, tag, "unexpected tag encountered while parsing table");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return tbl.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
|
||||
import com.sun.source.doctree.DocTree;
|
||||
import com.sun.source.doctree.DocTreeVisitor;
|
||||
import com.sun.source.doctree.EndElementTree;
|
||||
import com.sun.source.doctree.StartElementTree;
|
||||
import com.sun.source.doctree.TextTree;
|
||||
|
||||
/**
|
||||
* A {@link DocTree} for handling HTML<p/>
|
||||
*
|
||||
* This class allows for converting the HTML tags recursively in the same fashion
|
||||
* as the Javadoc tags.
|
||||
*/
|
||||
public final class HtmlDocTree implements DocTree {
|
||||
|
||||
private final HtmlTagKind kind;
|
||||
private final StartElementTree start;
|
||||
private final EndElementTree end;
|
||||
private final List<? extends DocTree> body;
|
||||
|
||||
/**
|
||||
* Gets an {@link HtmlDocTree} for the provided {@link StartElementTree}
|
||||
*
|
||||
* @param converter the html converter
|
||||
* @param start the html start
|
||||
* @param el the element containing the documentation being processed
|
||||
* @param it the iterator over the remaining tags
|
||||
* @return the created {@link HtmlDocTree}
|
||||
*/
|
||||
public static HtmlDocTree getTree(HtmlConverter converter, StartElementTree start, Element el,
|
||||
ListIterator<? extends DocTree> it) {
|
||||
HtmlTagKind kind = HtmlTagKind.getKind(start);
|
||||
List<DocTree> body = new ArrayList<>();
|
||||
if (start.isSelfClosing() || HtmlTagKind.isVoidTag(kind)) {
|
||||
return new HtmlDocTree(kind, start, null, body);
|
||||
}
|
||||
while (it.hasNext()) {
|
||||
DocTree tag = it.next();
|
||||
switch (tag.getKind()) {
|
||||
case START_ELEMENT:
|
||||
if (kind.isTerminateBy((StartElementTree) tag)) {
|
||||
// hack for unclosed elements
|
||||
it.previous();
|
||||
converter.logUnterminatedHtml(el, start);
|
||||
return new HtmlDocTree(kind, start, null, body);
|
||||
}
|
||||
body.add(HtmlDocTree.getTree(converter, (StartElementTree) tag, el, it));
|
||||
break;
|
||||
case END_ELEMENT:
|
||||
if (kind.isTerminateBy((EndElementTree) tag)) {
|
||||
// hack for unclosed elements
|
||||
it.previous();
|
||||
converter.logUnterminatedHtml(el, start);
|
||||
return new HtmlDocTree(kind, start, null, body);
|
||||
}
|
||||
if (kind == HtmlTagKind.getKind((EndElementTree) tag)) {
|
||||
return new HtmlDocTree(kind, start, (EndElementTree) tag, body);
|
||||
}
|
||||
body.add(tag);
|
||||
break;
|
||||
case TEXT:
|
||||
String text = ((TextTree) tag).getBody();
|
||||
if (kind != HtmlTagKind.PRE && text.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
body.add(tag);
|
||||
break;
|
||||
default:
|
||||
body.add(tag);
|
||||
break;
|
||||
}
|
||||
}
|
||||
converter.logUnterminatedHtml(el, start);
|
||||
return new HtmlDocTree(kind, start, null, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link HtmlDocTree} with the provided fields
|
||||
*
|
||||
* @param kind the html tag kind
|
||||
* @param start the start element
|
||||
* @param end the optional end element
|
||||
* @param body the html body
|
||||
*/
|
||||
private HtmlDocTree(HtmlTagKind kind, StartElementTree start, EndElementTree end,
|
||||
List<DocTree> body) {
|
||||
this.kind = kind;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.body = Collections.unmodifiableList(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind getKind() {
|
||||
// OTHER is implementation reserved
|
||||
// Since this is implementation specific, lets use it
|
||||
return Kind.OTHER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <R, D> R accept(DocTreeVisitor<R, D> visitor, D data) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html body
|
||||
*
|
||||
* @return the html body
|
||||
*/
|
||||
public List<? extends DocTree> getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html tag kind
|
||||
*
|
||||
* @return the html tag kind
|
||||
*/
|
||||
public HtmlTagKind getHtmlKind() {
|
||||
return kind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html start element tree
|
||||
*
|
||||
* @return the html start element
|
||||
*/
|
||||
public StartElementTree getStartTag() {
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the html end element tree<p/>
|
||||
*
|
||||
* This may be null if the html tag is a "void" tag or if the html is malformed
|
||||
*
|
||||
* @return the html end element or null
|
||||
*/
|
||||
public EndElementTree getEndTag() {
|
||||
return end;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,351 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.sun.source.doctree.EndElementTree;
|
||||
import com.sun.source.doctree.StartElementTree;
|
||||
|
||||
public enum HtmlTagKind {
|
||||
// This would be much simpler if we didn't have to handle malformed html
|
||||
// HTML container tags REQUIRE a closing tag
|
||||
// Unfortunately they are often ommitted, even in the JDK API, which makes
|
||||
// this much more complicated then it needs to be.
|
||||
// Best we can do it try not to consume elements that can't possibly be ours,
|
||||
// log it when encountered and then hope the result isn't ruined.
|
||||
|
||||
A,
|
||||
B,
|
||||
BIG,
|
||||
BLOCKQUOTE {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return kind == this;
|
||||
}
|
||||
},
|
||||
BR,
|
||||
CAPTION,
|
||||
CITE,
|
||||
CODE,
|
||||
DD {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case DD, DT, DL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
DEL,
|
||||
DFN,
|
||||
DIV,
|
||||
DL {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case DL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
DT {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case DD, DT, DL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
EM,
|
||||
H1 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
H2 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
H3 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
H4 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
H5 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
H6 {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
if (isInline(kind)) {
|
||||
return false;
|
||||
}
|
||||
return switch (kind) {
|
||||
case A -> false;
|
||||
default -> true;
|
||||
};
|
||||
}
|
||||
},
|
||||
HR,
|
||||
I,
|
||||
IMG,
|
||||
INS,
|
||||
LI {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case LI -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminateBy(EndElementTree end) {
|
||||
return switch (getKind(end)) {
|
||||
case OL, UL -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
OL {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
P,
|
||||
PRE {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
SMALL,
|
||||
SPAN,
|
||||
STRONG,
|
||||
SUB,
|
||||
SUP,
|
||||
TABLE {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
// no nested tables
|
||||
return kind == this;
|
||||
}
|
||||
},
|
||||
TBODY {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case THEAD, TFOOT, TBODY, TABLE -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
TD {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case TD, TH, TR, THEAD, TFOOT, TBODY, TABLE -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
TFOOT {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case THEAD, TFOOT, TBODY, TABLE -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
TH {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case TD, TH, TR, THEAD, TFOOT, TBODY, TABLE -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
THEAD {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case THEAD, TFOOT, TBODY, TABLE -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
TR {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case TR, TABLE, THEAD, TFOOT, TBODY -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
},
|
||||
TT,
|
||||
U,
|
||||
UL {
|
||||
@Override
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
VAR,
|
||||
UNSUPPORTED;
|
||||
|
||||
private static final Map<String, HtmlTagKind> LOOKUP;
|
||||
|
||||
static {
|
||||
HtmlTagKind[] values = values();
|
||||
LOOKUP = new HashMap<>(values.length);
|
||||
for (HtmlTagKind value : values) {
|
||||
LOOKUP.put(value.name(), value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HtmlTagKind with the provided name
|
||||
*
|
||||
* @param name the name
|
||||
* @return the HtmlTagKind with the same name or UNSUPPORTED
|
||||
*/
|
||||
static HtmlTagKind getKind(String name) {
|
||||
return LOOKUP.getOrDefault(name, UNSUPPORTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HtmlTagKind for the provided element
|
||||
*
|
||||
* @param tag the tag
|
||||
* @return the HtmlTagKind for the provided tag or UNSUPPORTED
|
||||
*/
|
||||
static HtmlTagKind getKind(StartElementTree tag) {
|
||||
return getKind(tag.getName().toString().toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the HtmlTagKind for the provided element
|
||||
*
|
||||
* @param tag the tag
|
||||
* @return the HtmlTagKind for the provided tag or UNSUPPORTED
|
||||
*/
|
||||
static HtmlTagKind getKind(EndElementTree tag) {
|
||||
return getKind(tag.getName().toString().toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this tag is terminated by another tag because it can't possibly contain it
|
||||
*
|
||||
* @param kind the other HtmlTagKind
|
||||
* @return true if this tag canot possibly contain the other kind
|
||||
*/
|
||||
boolean isTerminateBy(HtmlTagKind kind) {
|
||||
return !isInline(kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this tag is terminated by another element because it can't possibly contain it
|
||||
*
|
||||
* @param kind the other HtmlTagKind
|
||||
* @return true if this tag canot possibly contain the other element
|
||||
*/
|
||||
public final boolean isTerminateBy(StartElementTree start) {
|
||||
HtmlTagKind kind = getKind(start);
|
||||
return isTerminateBy(kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this tag is terminated by the closing another element.<p/>
|
||||
*
|
||||
* This is usually because the other element would contain it.
|
||||
*
|
||||
* @param kind the other HtmlTagKind
|
||||
* @return true if this tag canot possibly contain the other kind
|
||||
*/
|
||||
public boolean isTerminateBy(EndElementTree end) {
|
||||
HtmlTagKind kind = getKind(end);
|
||||
if (kind == this) {
|
||||
// this tag may not be for the current node so we return false here
|
||||
return false;
|
||||
}
|
||||
return isTerminateBy(kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided tag is a void or empty tag
|
||||
*
|
||||
* @param kind the tag kind
|
||||
* @return true if this is a void or empty tag
|
||||
*/
|
||||
public static boolean isVoidTag(HtmlTagKind kind) {
|
||||
// technically <p> is NOT a void tag
|
||||
// unfortunately it is misused so often that the errors/warnings
|
||||
// would become junk because the <p> tags would have consumed too much
|
||||
return switch (kind) {
|
||||
case BR, HR, P -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided tag is for inline markup
|
||||
*
|
||||
* @param kind the tag kind
|
||||
* @return true if this kind is for inline markup
|
||||
*/
|
||||
public static boolean isInline(HtmlTagKind kind) {
|
||||
return switch (kind) {
|
||||
case B, BIG, CITE, DFN, CODE, DEL, EM, I, INS -> true;
|
||||
case SMALL, STRONG, SUB, SUP, TT, U, VAR -> true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,681 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.PackageElement;
|
||||
import javax.lang.model.element.QualifiedNameable;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
import com.sun.source.doctree.*;
|
||||
|
||||
import jdk.javadoc.doclet.DocletEnvironment;
|
||||
import jdk.javadoc.doclet.Reporter;
|
||||
|
||||
/**
|
||||
* Helper class for converting Javadoc to Python docstring format
|
||||
*/
|
||||
public class JavadocConverter extends DocConverter {
|
||||
|
||||
private static final Pattern LEADING_WHITESPACE = Pattern.compile("(\\s+)\\S.*");
|
||||
|
||||
private static final Map<String, String> AUTO_CONVERSIONS = new HashMap<>(
|
||||
Map.ofEntries(
|
||||
Map.entry("java.lang.Boolean", "java.lang.Boolean or bool"),
|
||||
Map.entry("java.lang.Byte", "java.lang.Byte or int"),
|
||||
Map.entry("java.lang.Character", "java.lang.Character or int or str"),
|
||||
Map.entry("java.lang.Double", "java.lang.Double or float"),
|
||||
Map.entry("java.lang.Float", "java.lang.Float or float"),
|
||||
Map.entry("java.lang.Integer", "java.lang.Integer or int"),
|
||||
Map.entry("java.lang.Long", "java.lang.Long or int"),
|
||||
Map.entry("java.lang.Short", "java.lang.Short or int"),
|
||||
Map.entry("java.lang.String", "java.lang.String or str"),
|
||||
Map.entry("java.io.File", "jpype.protocol.SupportsPath"),
|
||||
Map.entry("java.nio.file.Path", "jpype.protocol.SupportsPath"),
|
||||
Map.entry("java.lang.Iterable", "collections.abc.Sequence"),
|
||||
Map.entry("java.util.Collection", "collections.abc.Sequence"),
|
||||
Map.entry("java.util.Map", "collections.abc.Mapping"),
|
||||
Map.entry("java.time.Instant", "datetime.datetime"),
|
||||
Map.entry("java.sql.Time", "datetime.time"),
|
||||
Map.entry("java.sql.Date", "datetime.date"),
|
||||
Map.entry("java.sql.Timestamp", "datetime.datetime"),
|
||||
Map.entry("java.math.BigDecimal", "decimal.Decimal")));
|
||||
|
||||
// these tags are used in the jdk and shouldn't cause any warnings
|
||||
// it is not worth the effort to handle them to output any documentation
|
||||
private static final Set<String> JDK_TAGLETS = new HashSet<>(
|
||||
Set.of("jls", "jvms", "extLink", "Incubating", "moduleGraph", "sealedGraph", "toolGuide"));
|
||||
|
||||
private static final Map<String, String> NOTE_TAGLETS = new HashMap<>(
|
||||
Map.of("apiNote", "API Note", "implNote", "Implementation Note", "implSpec",
|
||||
"Implementation Requirements"));
|
||||
|
||||
private final HtmlConverter htmlConverter;
|
||||
|
||||
/**
|
||||
* Creates a new {@link DocConverter}
|
||||
*
|
||||
* @param env the doclet environment
|
||||
* @param log the log
|
||||
*/
|
||||
public JavadocConverter(DocletEnvironment env, Reporter log) {
|
||||
super(env, log);
|
||||
this.htmlConverter = new HtmlConverter(env, log, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Javadoc for the provided element
|
||||
*
|
||||
* @param el the element
|
||||
* @return the Javadoc
|
||||
*/
|
||||
String getJavadoc(Element el) {
|
||||
return getJavadoc(el, getDocTrees().getDocCommentTree(el));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Javadoc tree for the provided element
|
||||
*
|
||||
* @param el the element
|
||||
* @return the Javadoc tree
|
||||
*/
|
||||
DocCommentTree getJavadocTree(Element el) {
|
||||
return getDocTrees().getDocCommentTree(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the converted documentation for the provided element and doc tree
|
||||
*
|
||||
* @param el the element
|
||||
* @param docCommentTree the doc tree
|
||||
* @return the converted documentation
|
||||
*/
|
||||
private String getJavadoc(Element el, DocCommentTree docCommentTree) {
|
||||
if (docCommentTree != null) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
ListIterator<? extends DocTree> it = docCommentTree.getFullBody().listIterator();
|
||||
while (it.hasNext()) {
|
||||
DocTree next = it.next();
|
||||
builder.append(convertTag(el, next, it));
|
||||
}
|
||||
// A blank line is required before block tags
|
||||
builder.append("\n\n");
|
||||
List<SeeTree> seealso = new ArrayList<>();
|
||||
it = docCommentTree.getBlockTags().listIterator();
|
||||
while (it.hasNext()) {
|
||||
DocTree tag = it.next();
|
||||
if (tag.getKind() == DocTree.Kind.SEE) {
|
||||
seealso.add((SeeTree) tag);
|
||||
continue;
|
||||
}
|
||||
if (tag.getKind() == DocTree.Kind.HIDDEN) {
|
||||
// hidden blocktag means don't document
|
||||
return "";
|
||||
}
|
||||
builder.append(convertTag(el, tag, it));
|
||||
}
|
||||
if (!seealso.isEmpty()) {
|
||||
builder.append("\n.. seealso::\n\n");
|
||||
for (SeeTree tag : seealso) {
|
||||
String message = "| " + alignIndent(convertTree(el, tag.getReference()));
|
||||
builder.append(message.indent(INDENT_WIDTH))
|
||||
.append('\n');
|
||||
}
|
||||
|
||||
}
|
||||
String tmp = builder.toString().replaceAll("\t", " ");
|
||||
if (tmp.indexOf('\n') == -1) {
|
||||
return tmp;
|
||||
}
|
||||
builder = new StringBuilder(tmp.length());
|
||||
|
||||
// we need to fix the indentation because it will mess with the reStructured text
|
||||
// NOTE: you cannot just use String.stripLeading or String.indent(-1) here
|
||||
Iterable<String> lines = () -> tmp.lines().iterator();
|
||||
for (String line : lines) {
|
||||
Matcher matcher = LEADING_WHITESPACE.matcher(line);
|
||||
if (matcher.matches()) {
|
||||
String whitespace = matcher.group(1);
|
||||
builder.append(line.substring(whitespace.length() % INDENT_WIDTH))
|
||||
.append('\n');
|
||||
}
|
||||
else {
|
||||
builder.append(line)
|
||||
.append('\n');
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
String convertTag(Element el, DocTree tag, ListIterator<? extends DocTree> it) {
|
||||
// NOTE: each tag is responsible for its own line endings
|
||||
return switch (tag.getKind()) {
|
||||
case DOC_ROOT -> tag.toString(); // not sure what would be an appropriate replacement
|
||||
case PARAM -> convertParamTag(el, (ParamTree) tag);
|
||||
case RETURN -> convertReturnTag((ExecutableElement) el, (ReturnTree) tag);
|
||||
case THROWS -> convertThrowsTag((ExecutableElement) el, (ThrowsTree) tag);
|
||||
case START_ELEMENT -> convertHTML(el, (StartElementTree) tag, it);
|
||||
case END_ELEMENT -> convertHTML((EndElementTree) tag);
|
||||
case LINK -> convertLinkTag(el, (LinkTree) tag);
|
||||
case LINK_PLAIN -> convertLinkTag(el, (LinkTree) tag);
|
||||
case EXCEPTION -> convertThrowsTag((ExecutableElement) el, (ThrowsTree) tag);
|
||||
case ENTITY -> convertEntity((EntityTree) tag);
|
||||
case CODE -> convertCodeTag((LiteralTree) tag);
|
||||
case LITERAL -> convertLiteralTag((LiteralTree) tag);
|
||||
case VALUE -> convertValueTag(el, (ValueTree) tag);
|
||||
case DEPRECATED -> convertDeprecatedTag(el, (DeprecatedTree) tag);
|
||||
case REFERENCE -> convertReferenceTag(el, (ReferenceTree) tag);
|
||||
case SINCE -> convertSinceTag(el, (SinceTree) tag);
|
||||
case AUTHOR -> convertAuthorTag(el, (AuthorTree) tag);
|
||||
case VERSION -> ""; // ignored
|
||||
case ERRONEOUS -> {
|
||||
logError(el, tag, "erroneous javadoc tag");
|
||||
yield tag.toString();
|
||||
}
|
||||
case UNKNOWN_BLOCK_TAG -> convertUnknownBlockTag(el, (UnknownBlockTagTree) tag);
|
||||
case UNKNOWN_INLINE_TAG -> {
|
||||
if (JDK_TAGLETS.contains(((UnknownInlineTagTree) tag).getTagName())) {
|
||||
yield "";
|
||||
}
|
||||
logError(el, tag, "unknown javadoc inline tag");
|
||||
yield tag.toString();
|
||||
}
|
||||
case TEXT -> ((TextTree) tag).getBody();
|
||||
case SNIPPET -> convertSnippet(el, (SnippetTree) tag);
|
||||
case INHERIT_DOC -> ""; // ignored, anything containing this is skipped
|
||||
case OTHER -> {
|
||||
if (tag instanceof HtmlDocTree html) {
|
||||
yield htmlConverter.convertHtml(html, el, it);
|
||||
}
|
||||
else {
|
||||
yield tag.toString();
|
||||
}
|
||||
}
|
||||
case SPEC -> "";
|
||||
case SERIAL -> "";
|
||||
case SERIAL_DATA -> "";
|
||||
case SYSTEM_PROPERTY -> "``" + ((SystemPropertyTree) tag).getPropertyName() + "``";
|
||||
case COMMENT -> "";
|
||||
case INDEX -> "";
|
||||
default -> {
|
||||
logWarning(el, tag, "unsupported javadoc tag");
|
||||
yield tag.toString();
|
||||
}
|
||||
case ESCAPE -> ((EscapeTree) tag).getBody();
|
||||
case SERIAL_FIELD -> "";
|
||||
case SUMMARY -> convertTree(el, ((SummaryTree) tag).getSummary());
|
||||
case USES -> "";
|
||||
};
|
||||
}
|
||||
|
||||
private String convertUnknownBlockTag(Element el, UnknownBlockTagTree tag) {
|
||||
if (JDK_TAGLETS.contains(tag.getTagName())) {
|
||||
return "";
|
||||
}
|
||||
String title = NOTE_TAGLETS.get(tag.getTagName());
|
||||
if (title == null) {
|
||||
logError(el, tag, "unknown javadoc block tag");
|
||||
return tag.toString();
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String message = alignIndent(convertTree(el, tag.getContent()));
|
||||
return builder.append("\n.. admonition:: ")
|
||||
.append(title)
|
||||
.append("\n\n")
|
||||
.append(message.indent(INDENT_WIDTH))
|
||||
.append("\n\n")
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the attributes for the provided snippet
|
||||
*
|
||||
* @param snippet the snippet
|
||||
* @return the snippet attributes
|
||||
*/
|
||||
private Map<String, String> getAttributes(Element el, SnippetTree snippet) {
|
||||
return getAttributes(el, snippet.getAttributes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Indent the provided text
|
||||
*
|
||||
* @param text the text to indent
|
||||
* @return the indented text
|
||||
*/
|
||||
private static String indent(String text) {
|
||||
return text.indent(INDENT_WIDTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indent the provided text tree
|
||||
*
|
||||
* @param text the text tree
|
||||
* @return the indented text
|
||||
*/
|
||||
private static String indent(TextTree text) {
|
||||
return indent(text.getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an author Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param author the author tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertAuthorTag(Element el, AuthorTree author) {
|
||||
String name = convertTree(el, author.getName());
|
||||
return "\n.. codeauthor:: " + name + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a since Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param since the since tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertSinceTag(Element el, SinceTree since) {
|
||||
// NOTE: there must be a preceeding new line
|
||||
String msg = convertTree(el, since.getBody());
|
||||
return "\n.. versionadded:: " + msg + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a link Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param link the link tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertLinkTag(Element el, LinkTree link) {
|
||||
String sig = link.getReference().getSignature().replaceAll("#", ".");
|
||||
int index = sig.indexOf('(');
|
||||
String label = convertTree(el, link.getLabel());
|
||||
if (index != -1) {
|
||||
String name = sig;
|
||||
sig = sig.substring(0, index);
|
||||
if (label.isBlank()) {
|
||||
if (name.startsWith(".")) {
|
||||
label = name.substring(1);
|
||||
}
|
||||
else {
|
||||
label = name;
|
||||
}
|
||||
}
|
||||
return ":meth:`" + label + " <" + sig + ">`";
|
||||
}
|
||||
if (!label.isBlank()) {
|
||||
return ":obj:`" + label + " <" + sig + ">`";
|
||||
}
|
||||
return ":obj:`" + sig + '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the constant value for a value tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the value tag
|
||||
* @return the constant value
|
||||
*/
|
||||
private static String getConstantValue(VariableElement el, ValueTree tag) {
|
||||
Object value = el.getConstantValue();
|
||||
TextTree format = tag.getFormat();
|
||||
if (format != null) {
|
||||
try {
|
||||
return String.format(format.getBody(), value);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// fallthrough
|
||||
}
|
||||
}
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a Javadoc reference
|
||||
*
|
||||
* @param el the current element
|
||||
* @param ref the reference
|
||||
* @return the converted reference
|
||||
*/
|
||||
private String convertReferenceTag(Element el, ReferenceTree ref) {
|
||||
String sig = ref.getSignature();
|
||||
if (sig == null || sig.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return ":obj:`" + sig.replace('#', '.') + '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a value Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param value the value tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertValueTag(Element el, ValueTree value) {
|
||||
ReferenceTree ref = value.getReference();
|
||||
if (ref == null) {
|
||||
return "";
|
||||
}
|
||||
String sig = ref.getSignature();
|
||||
if (sig == null || sig.isBlank()) {
|
||||
if (el instanceof VariableElement var) {
|
||||
return getConstantValue(var, value);
|
||||
}
|
||||
return ":const:`" + sig.replaceAll("#", ".") + '`';
|
||||
}
|
||||
int index = sig.indexOf('#');
|
||||
TypeElement type;
|
||||
String field;
|
||||
if (index == 0) {
|
||||
if (el instanceof ExecutableElement method) {
|
||||
type = (TypeElement) method.getEnclosingElement();
|
||||
}
|
||||
else {
|
||||
type = (TypeElement) el;
|
||||
}
|
||||
field = sig.substring(1);
|
||||
}
|
||||
else {
|
||||
String name = sig.substring(0, index);
|
||||
type = getElementUtils().getTypeElement(name);
|
||||
if (type == null && el instanceof ExecutableElement method) {
|
||||
// check if the name of the current class was specified
|
||||
type = (TypeElement) method.getEnclosingElement();
|
||||
if (!type.getSimpleName().contentEquals(name)) {
|
||||
type = null;
|
||||
}
|
||||
}
|
||||
field = sig.substring(index + 1);
|
||||
}
|
||||
if (type != null) {
|
||||
for (Element child : getElementUtils().getAllMembers(type)) {
|
||||
if (child.getSimpleName().contentEquals(field)) {
|
||||
if (child instanceof VariableElement var) {
|
||||
return getConstantValue(var, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ":const:`" + sig.replaceAll("#", ".") + '`';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a deprecated Javadoc tag
|
||||
*
|
||||
* @param tag the deprecated tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertDeprecatedTag(Element el, DeprecatedTree tag) {
|
||||
String body = convertTree(el, tag.getBody());
|
||||
return new StringBuilder("\n.. deprecated::\n\n")
|
||||
.append(body)
|
||||
.append('\n')
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a snippet Javadoc tag
|
||||
*
|
||||
* @param snippet the snippet tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertSnippet(Element el, SnippetTree snippet) {
|
||||
// let pygments guess the code type
|
||||
TextTree body = snippet.getBody();
|
||||
if (body == null) {
|
||||
// there are invalid snippet tags in the internal jdk packages
|
||||
return "";
|
||||
}
|
||||
|
||||
Map<String, String> attributes = getAttributes(el, snippet);
|
||||
String lang = attributes.getOrDefault("lang", "guess");
|
||||
// any other attributes are not supported
|
||||
return new StringBuilder(".. code-block:: ")
|
||||
.append(lang)
|
||||
.append("\n :dedent: 4\n\n")
|
||||
.append(indent(body))
|
||||
.append('\n')
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a code Javadoc tag
|
||||
*
|
||||
* @param code the code tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private static String convertCodeTag(LiteralTree code) {
|
||||
String body = convertLiteralTag(code);
|
||||
if (body.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return "``" + body + "``";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a literal Javadoc tag
|
||||
*
|
||||
* @param literal the literal tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private static String convertLiteralTag(LiteralTree literal) {
|
||||
// NOTE: the literal tag DOES NOT preserve line endings or whitespace
|
||||
// it is still present in the body so remove it
|
||||
TextTree text = literal.getBody();
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String body = text.getBody();
|
||||
if (body == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return body.stripIndent().replaceAll("\n", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a html entity (ie. {@literal &lt;})
|
||||
*
|
||||
* @param entity the entity
|
||||
* @return the converted entity
|
||||
*/
|
||||
private String convertEntity(EntityTree entity) {
|
||||
return getDocTrees().getCharacters(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a html tag
|
||||
*
|
||||
* @param tag the html start tag
|
||||
* @return the converted html
|
||||
*/
|
||||
private String convertHTML(Element el, StartElementTree tag,
|
||||
ListIterator<? extends DocTree> it) {
|
||||
return htmlConverter.convertHtml(tag, el, it);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a html tag
|
||||
*
|
||||
* @param tag the html end tag
|
||||
* @return the converted html
|
||||
*/
|
||||
private static String convertHTML(EndElementTree tag) {
|
||||
if (tag.getName().contentEquals("p")) {
|
||||
return "\n";
|
||||
}
|
||||
return tag.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the provided type with respect to the provided method element
|
||||
*
|
||||
* @param el the method element
|
||||
* @param type the type
|
||||
* @return the sanitized type name
|
||||
*/
|
||||
private static String sanitizeQualifiedName(ExecutableElement el, TypeMirror type) {
|
||||
Element self = el.getEnclosingElement();
|
||||
PackageElement pkg = PythonTypeStubElement.getPackage(self);
|
||||
return PythonTypeStubElement.sanitizeQualifiedName(self, type, pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a param Javadoc tag for a method parameter
|
||||
*
|
||||
* @param el the current element
|
||||
* @param param the param tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertParamTag(Element el, ParamTree param) {
|
||||
if (el instanceof ExecutableElement executableElement) {
|
||||
return convertParamTag(executableElement, param);
|
||||
}
|
||||
return convertParamTag((TypeElement) el, param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a param Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param param the param tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private static String convertParamTag(TypeElement el, ParamTree param) {
|
||||
// I'm not sure python does this?
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the parameter type type to show all possible values
|
||||
*
|
||||
* @param type the type to convert
|
||||
* @return the type or null if not applicable
|
||||
*/
|
||||
private static String convertParamType(TypeMirror type) {
|
||||
if (type.getKind().isPrimitive()) {
|
||||
return switch (type.getKind()) {
|
||||
case BOOLEAN -> "jpype.JBoolean or bool";
|
||||
case BYTE -> "jpype.JByte or int";
|
||||
case CHAR -> "jpype.JChar or int or str";
|
||||
case DOUBLE -> "jpype.JDouble or float";
|
||||
case FLOAT -> "jpype.JFloat or float";
|
||||
case INT -> "jpype.JInt or int";
|
||||
case LONG -> "jpype.JLong or int";
|
||||
case SHORT -> "jpype.JShort or int";
|
||||
default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
|
||||
};
|
||||
}
|
||||
if (type instanceof DeclaredType dt) {
|
||||
Element element = dt.asElement();
|
||||
if (element instanceof QualifiedNameable nameable) {
|
||||
return AUTO_CONVERSIONS.get(nameable.getQualifiedName().toString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a param Javadoc tag for a method parameter
|
||||
*
|
||||
* @param el the current element
|
||||
* @param param the param tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertParamTag(ExecutableElement el, ParamTree param) {
|
||||
TypeMirror type = null;
|
||||
for (VariableElement child : el.getParameters()) {
|
||||
if (child.getSimpleName().equals(param.getName().getName())) {
|
||||
type = child.asType();
|
||||
break;
|
||||
}
|
||||
}
|
||||
String description = convertTree(el, param.getDescription());
|
||||
if (type == null) {
|
||||
return ":param " + param.getName() + ": " + description;
|
||||
}
|
||||
String typename = convertParamType(type);
|
||||
if (typename == null) {
|
||||
typename = sanitizeQualifiedName(el, type);
|
||||
}
|
||||
return ":param " + typename + " " + param.getName() + ": " + description + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a return Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the return tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertReturnTag(ExecutableElement el, ReturnTree tag) {
|
||||
String description = convertTree(el, tag.getDescription());
|
||||
if (el.getReturnType().getKind() == TypeKind.VOID) {
|
||||
return ":return: " + description + '\n';
|
||||
}
|
||||
|
||||
String typename = PythonTypeStubMethod.convertResultType(el.getReturnType());
|
||||
if (typename == null) {
|
||||
typename = sanitizeQualifiedName(el, el.getReturnType());
|
||||
}
|
||||
String res = ":return: " + description + '\n';
|
||||
return res + ":rtype: " + typename + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a throws Javadoc tag
|
||||
*
|
||||
* @param el the current element
|
||||
* @param tag the throws tag
|
||||
* @return the converted tag
|
||||
*/
|
||||
private String convertThrowsTag(ExecutableElement el, ThrowsTree tag) {
|
||||
String typename = tag.getExceptionName().getSignature();
|
||||
TypeMirror type = null;
|
||||
for (TypeMirror thrownType : el.getThrownTypes()) {
|
||||
if (thrownType.getKind() == TypeKind.TYPEVAR) {
|
||||
if (thrownType.toString().equals(typename)) {
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
TypeElement typeElement = (TypeElement) (((DeclaredType) thrownType).asElement());
|
||||
if (typeElement.getQualifiedName().contentEquals(typename)) {
|
||||
type = thrownType;
|
||||
break;
|
||||
}
|
||||
if (typeElement.getQualifiedName().toString().startsWith("java.lang.")) {
|
||||
if (typeElement.getSimpleName().contentEquals(typename)) {
|
||||
type = thrownType;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (type != null) {
|
||||
typename = sanitizeQualifiedName(el, type);
|
||||
}
|
||||
String description = convertTree(el, tag.getDescription());
|
||||
return ":raises " + typename + ": " + description + '\n';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,526 @@
|
|||
/* ###
|
||||
* IP: GHIDRA
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import javax.lang.model.SourceVersion;
|
||||
import javax.lang.model.element.*;
|
||||
import javax.lang.model.util.ElementFilter;
|
||||
import javax.lang.model.util.Elements;
|
||||
import javax.lang.model.util.Types;
|
||||
import javax.tools.Diagnostic.Kind;
|
||||
|
||||
import com.sun.source.doctree.DeprecatedTree;
|
||||
import com.sun.source.doctree.DocCommentTree;
|
||||
import com.sun.source.doctree.DocTree;
|
||||
import com.sun.source.doctree.LinkTree;
|
||||
import com.sun.source.doctree.StartElementTree;
|
||||
import com.sun.source.doctree.TextTree;
|
||||
|
||||
import jdk.javadoc.doclet.*;
|
||||
|
||||
/**
|
||||
* Doclet that outputs Python pyi files.<p/>
|
||||
*
|
||||
* To run: gradle createPythonTypeStubs
|
||||
*/
|
||||
public class PythonTypeStubDoclet implements Doclet {
|
||||
|
||||
private Reporter log;
|
||||
private File destDir;
|
||||
|
||||
private DocletEnvironment docEnv;
|
||||
private JavadocConverter docConverter;
|
||||
private Set<String> processedPackages;
|
||||
private Set<String> topLevelPackages;
|
||||
private boolean useAllTypes = false;
|
||||
private boolean useProperties = true;
|
||||
private boolean ghidraMode = false;
|
||||
|
||||
@Override
|
||||
public void init(Locale locale, Reporter reporter) {
|
||||
this.log = reporter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SourceVersion getSupportedSourceVersion() {
|
||||
return SourceVersion.RELEASE_21;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<? extends Option> getSupportedOptions() {
|
||||
return Set.of(new Option() {
|
||||
@Override
|
||||
public int getArgumentCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "the destination directory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind getKind() {
|
||||
return Option.Kind.STANDARD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNames() {
|
||||
return Arrays.asList("-d");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameters() {
|
||||
return "directory";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(String option, List<String> arguments) {
|
||||
destDir = new File(arguments.get(0)).getAbsoluteFile();
|
||||
return true;
|
||||
}
|
||||
|
||||
},
|
||||
new Option() {
|
||||
@Override
|
||||
public int getArgumentCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "enables Ghidra specific output";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind getKind() {
|
||||
return Option.Kind.OTHER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNames() {
|
||||
return Arrays.asList("-ghidra");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameters() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(String option, List<String> arguments) {
|
||||
ghidraMode = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
},
|
||||
new Option() {
|
||||
@Override
|
||||
public int getArgumentCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "enables generation of properties from get/set/is methods";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Kind getKind() {
|
||||
return Option.Kind.OTHER;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getNames() {
|
||||
return Arrays.asList("-properties");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameters() {
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean process(String option, List<String> arguments) {
|
||||
useProperties = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean run(DocletEnvironment env) {
|
||||
|
||||
docEnv = env;
|
||||
docConverter = new JavadocConverter(env, log);
|
||||
|
||||
processedPackages = new HashSet<>();
|
||||
topLevelPackages = new HashSet<>();
|
||||
|
||||
// Create destination directory
|
||||
if (destDir == null) {
|
||||
log.print(Kind.ERROR, "Destination directory not set");
|
||||
return false;
|
||||
}
|
||||
if (!destDir.exists()) {
|
||||
if (!destDir.mkdirs()) {
|
||||
log.print(Kind.ERROR, "Failed to create destination directory at: " + destDir);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Elements elements = docEnv.getElementUtils();
|
||||
Set<ModuleElement> modules = ElementFilter.modulesIn(docEnv.getSpecifiedElements());
|
||||
if (!modules.isEmpty()) {
|
||||
useAllTypes = true;
|
||||
modules.stream()
|
||||
.map(ModuleElement::getDirectives)
|
||||
.flatMap(List::stream)
|
||||
// only exported packages
|
||||
.filter(d -> d.getKind() == ModuleElement.DirectiveKind.EXPORTS)
|
||||
.map(ModuleElement.ExportsDirective.class::cast)
|
||||
// only exported to ALL-UNNAMED
|
||||
.filter(export -> export.getTargetModules() == null)
|
||||
.map(ModuleElement.ExportsDirective::getPackage)
|
||||
.map((el) -> new PythonTypeStubPackage(this, el))
|
||||
.forEach(PythonTypeStubPackage::process);
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<PackageElement> packages = ElementFilter.packagesIn(docEnv.getSpecifiedElements());
|
||||
if (!packages.isEmpty()) {
|
||||
useAllTypes = true;
|
||||
packages.stream()
|
||||
.map((el) -> new PythonTypeStubPackage(this, el))
|
||||
.forEach(PythonTypeStubPackage::process);
|
||||
return true;
|
||||
}
|
||||
|
||||
// it is not safe to use parallelStream :(
|
||||
ElementFilter.typesIn(docEnv.getSpecifiedElements())
|
||||
.stream()
|
||||
.map(elements::getPackageOf)
|
||||
.distinct()
|
||||
.map((el) -> new PythonTypeStubPackage(this, el))
|
||||
.forEach(PythonTypeStubPackage::process);
|
||||
|
||||
// ghidra docs always explicitly specifies the types
|
||||
// so we only need to check the option here
|
||||
if (ghidraMode) {
|
||||
GhidraBuiltinsBuilder builder = new GhidraBuiltinsBuilder(this);
|
||||
builder.process();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints all the imports in the provided collection<p/>
|
||||
*
|
||||
* If a provided import is not included in the output of this doclet, "#type: ignore"
|
||||
* will be appended to the import. This prevents the type checker from treating the
|
||||
* import as an error if the package is not found.
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param packages the packages to import
|
||||
*/
|
||||
void printImports(PrintWriter printer, Collection<PackageElement> packages) {
|
||||
for (PackageElement pkg : packages) {
|
||||
String name = PythonTypeStubElement.sanitizeQualifiedName(pkg);
|
||||
printer.print("import ");
|
||||
printer.print(name);
|
||||
if (!isIncluded(pkg)) {
|
||||
printer.println(" # type: ignore");
|
||||
}
|
||||
else {
|
||||
printer.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is deprecated
|
||||
*
|
||||
* @param el the element to check
|
||||
* @return true if the element is deprecated
|
||||
*/
|
||||
boolean isDeprecated(Element el) {
|
||||
return docEnv.getElementUtils().isDeprecated(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ElementUtils for the current doclet environment
|
||||
*
|
||||
* @return the ElementUtils
|
||||
*/
|
||||
Elements getElementUtils() {
|
||||
return docEnv.getElementUtils();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an appropriate message to be used in the warnings.deprecated decorator
|
||||
*
|
||||
* @param el the deprecated element
|
||||
* @return the deprecation message or null if no deprecation reason is documented
|
||||
*/
|
||||
String getDeprecatedMessage(Element el) {
|
||||
DocCommentTree tree = docConverter.getJavadocTree(el);
|
||||
if (tree == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DeprecatedTree deprecatedTag = tree.getBlockTags()
|
||||
.stream()
|
||||
.filter(tag -> tag.getKind() == DocTree.Kind.DEPRECATED)
|
||||
.map(DeprecatedTree.class::cast)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (deprecatedTag == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String res = getPlainDocString(deprecatedTag.getBody());
|
||||
// NOTE: this must be a safe string literal
|
||||
return getStringLiteral(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is specified to be included by this doclet
|
||||
*
|
||||
* @param element the element to check
|
||||
* @return
|
||||
*/
|
||||
boolean isSpecified(Element element) {
|
||||
return useAllTypes || docEnv.getSpecifiedElements().contains(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TypeUtils for the current doclet environment
|
||||
*
|
||||
* @return the TypeUtils
|
||||
*/
|
||||
Types getTypeUtils() {
|
||||
return docEnv.getTypeUtils();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output directory for the doclet
|
||||
*
|
||||
* @return the output directory
|
||||
*/
|
||||
File getDestDir() {
|
||||
return destDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the documentation for the provided element
|
||||
*
|
||||
* @param el the element
|
||||
* @return the elements documentation
|
||||
*/
|
||||
String getJavadoc(Element el) {
|
||||
return docConverter.getJavadoc(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this element has any documentation
|
||||
*
|
||||
* @param el the element
|
||||
* @return true if this element has documentation
|
||||
*/
|
||||
boolean hasJavadoc(Element el) {
|
||||
DocCommentTree tree = docConverter.getJavadocTree(el);
|
||||
if (tree == null) {
|
||||
return false;
|
||||
}
|
||||
return !tree.getFullBody().toString().isBlank();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this element has the provided Javadoc tag
|
||||
*
|
||||
* @param el the element
|
||||
* @param kind the tag kind
|
||||
* @return true if this element uses the provided Javadoc tag
|
||||
*/
|
||||
boolean hasJavadocTag(Element el, DocTree.Kind kind) {
|
||||
DocCommentTree tree = docConverter.getJavadocTree(el);
|
||||
if (tree == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Optional<?> res = tree.getFullBody()
|
||||
.stream()
|
||||
.map(DocTree::getKind)
|
||||
.filter(kind::equals)
|
||||
.findFirst();
|
||||
|
||||
if (res.isPresent()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return tree.getBlockTags()
|
||||
.stream()
|
||||
.map(DocTree::getKind)
|
||||
.filter(kind::equals)
|
||||
.findFirst()
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided package to the set of processed packages<p/>
|
||||
*
|
||||
* This will create any additional required namespace packages
|
||||
*
|
||||
* @param pkg the package being processed
|
||||
*/
|
||||
void addProcessedPackage(PackageElement pkg) {
|
||||
String name = pkg.getQualifiedName().toString();
|
||||
addProcessedPackage(PythonTypeStubElement.sanitizeQualifiedName(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the properties or ghidra options have been enabled
|
||||
*
|
||||
* @return true if either options are enabled
|
||||
*/
|
||||
boolean isUsingPythonProperties() {
|
||||
return useProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an appropriate string literal for the provided value<p/>
|
||||
*
|
||||
* The resulting String contains the value as required to be used in Java source code
|
||||
*
|
||||
* @param value the constant value
|
||||
* @return an appropriate String literal for the constant value
|
||||
*/
|
||||
String getStringLiteral(Object value) {
|
||||
return docEnv.getElementUtils().getConstantExpression(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided package is included in the doclet output
|
||||
*
|
||||
* @param el the package element
|
||||
* @return true if the package is included
|
||||
*/
|
||||
private boolean isIncluded(PackageElement el) {
|
||||
return docEnv.isIncluded(el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a namespace package for the provided package if one does not yet exist
|
||||
*
|
||||
* @param pkg the package to create
|
||||
*/
|
||||
private void createNamespacePackage(String pkg) {
|
||||
int index = pkg.indexOf('.');
|
||||
if (index != -1) {
|
||||
pkg = pkg.substring(0, index) + "-stubs" + pkg.substring(index);
|
||||
}
|
||||
else {
|
||||
pkg += "-stubs";
|
||||
}
|
||||
|
||||
File fp = new File(destDir, pkg.replace('.', '/') + "/__init__.pyi");
|
||||
try {
|
||||
fp.getParentFile().mkdirs();
|
||||
fp.createNewFile();
|
||||
}
|
||||
catch (IOException e) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the provided package to the set of processed packages<p/>
|
||||
*
|
||||
* A namespace package will be created if necessary
|
||||
*
|
||||
* @param pkg the package being processed
|
||||
*/
|
||||
private void addProcessedPackage(String pkg) {
|
||||
if (processedPackages.add(pkg)) {
|
||||
createNamespacePackage(pkg);
|
||||
int index = pkg.lastIndexOf('.');
|
||||
if (index != -1) {
|
||||
addProcessedPackage(pkg.substring(0, index));
|
||||
}
|
||||
else {
|
||||
topLevelPackages.add(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the docstring for the provided tags without markup
|
||||
*
|
||||
* @param tags the list of doclet tags
|
||||
* @return the docstring without any markup
|
||||
*/
|
||||
private static String getPlainDocString(List<? extends DocTree> tags) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
int ignoreDepth = 0;
|
||||
for (DocTree tag : tags) {
|
||||
switch (tag.getKind()) {
|
||||
case LINK:
|
||||
case LINK_PLAIN:
|
||||
LinkTree link = (LinkTree) tag;
|
||||
List<? extends DocTree> label = link.getLabel();
|
||||
if (!label.isEmpty()) {
|
||||
builder.append(getPlainDocString(label));
|
||||
}
|
||||
else {
|
||||
String sig = link.getReference().getSignature().replaceAll("#", ".");
|
||||
if (sig.startsWith(".")) {
|
||||
sig = sig.substring(1);
|
||||
}
|
||||
builder.append(sig);
|
||||
}
|
||||
break;
|
||||
case TEXT:
|
||||
TextTree text = (TextTree) tag;
|
||||
if (ignoreDepth == 0) {
|
||||
builder.append(text.getBody());
|
||||
}
|
||||
break;
|
||||
case START_ELEMENT:
|
||||
StartElementTree start = (StartElementTree) tag;
|
||||
if (!start.isSelfClosing()) {
|
||||
ignoreDepth++;
|
||||
}
|
||||
break;
|
||||
case END_ELEMENT:
|
||||
ignoreDepth--;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,428 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ElementKind;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.Name;
|
||||
import javax.lang.model.element.PackageElement;
|
||||
import javax.lang.model.element.QualifiedNameable;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
import javax.lang.model.type.WildcardType;
|
||||
|
||||
/**
|
||||
* Base class providing access to sanitized names (Python safe).
|
||||
*/
|
||||
abstract class PythonTypeStubElement<T extends Element> {
|
||||
|
||||
private static final Set<String> PY_KEYWORDS = new HashSet<>(
|
||||
Set.of("False", "None", "True", "and", "as", "assert", "async", "await", "break",
|
||||
"class", "continue", "def", "del", "elif", "else", "except", "exec", "finally", "for",
|
||||
"from", "global", "if", "import", "in", "is", "lambda",
|
||||
"nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
|
||||
"yield"));
|
||||
|
||||
static final String DOC_QUOTES = "\"\"\"";
|
||||
static final String ALT_DOC_QUOTES = "'''";
|
||||
static final String PY_INDENT = " ";
|
||||
|
||||
final PythonTypeStubDoclet doclet;
|
||||
final T el;
|
||||
private final PackageElement pkg;
|
||||
|
||||
private String name;
|
||||
|
||||
PythonTypeStubElement(PythonTypeStubDoclet doclet, T el) {
|
||||
this(doclet, getPackage(el), el);
|
||||
}
|
||||
|
||||
PythonTypeStubElement(PythonTypeStubDoclet doclet, PackageElement pkg, T el) {
|
||||
this.doclet = doclet;
|
||||
this.pkg = pkg;
|
||||
this.el = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the package for the provided element
|
||||
*
|
||||
* @param el the element
|
||||
* @return the package
|
||||
*/
|
||||
static PackageElement getPackage(Element el) {
|
||||
while (!(el instanceof PackageElement)) {
|
||||
el = el.getEnclosingElement();
|
||||
}
|
||||
return (PackageElement) el;
|
||||
}
|
||||
|
||||
static int compareQualifiedNameable(QualifiedNameable a, QualifiedNameable b) {
|
||||
return a.getQualifiedName().toString().compareTo(b.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is in the same package as this element
|
||||
*
|
||||
* @param el the other element
|
||||
* @return true if the other element is declared in the same package
|
||||
*/
|
||||
boolean isSamePackage(Element el) {
|
||||
return pkg.equals(getPackage(el));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided type is in the same package as this element
|
||||
*
|
||||
* @param type the type
|
||||
* @return true if the type is declared in the same package
|
||||
*/
|
||||
boolean isSamePackage(TypeMirror type) {
|
||||
if (type instanceof DeclaredType dt) {
|
||||
return pkg.equals(getPackage(dt.asElement()));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type string for the provided type and quotes if necessary<p/>
|
||||
*
|
||||
* This string value is safe to be used as a parameter or return type
|
||||
* as well as for use in a generic type.
|
||||
*
|
||||
* @param self the type to become typing.Self if encountered
|
||||
* @param type the type to get the string for
|
||||
* @return the type string
|
||||
*/
|
||||
String getTypeString(Element self, TypeMirror type) {
|
||||
String typeName = sanitizeQualifiedName(self, type);
|
||||
if (isSamePackage(type) && !typeName.equals("typing.Self")) {
|
||||
typeName = '"' + typeName + '"';
|
||||
}
|
||||
return typeName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Python safe name for this element
|
||||
*
|
||||
* @return the python safe name
|
||||
*/
|
||||
final String getName() {
|
||||
if (name == null) {
|
||||
name = sanitize(el.getSimpleName());
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Javadoc for the provided element to the provided printer
|
||||
*
|
||||
* @param element the element to write the javadoc for
|
||||
* @param printer the printer to write to
|
||||
* @param indent the indentation
|
||||
* @param emptyValue the value to use when there is no documentation
|
||||
* @return true if a Javadoc was written else false
|
||||
*/
|
||||
final boolean writeJavaDoc(Element element, PrintWriter printer, String indent,
|
||||
String emptyValue) {
|
||||
String doc = doclet.getJavadoc(element);
|
||||
if (doc.isBlank()) {
|
||||
if (!emptyValue.isBlank()) {
|
||||
printer.print(indent);
|
||||
printer.print(emptyValue);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
String quotes = doc.contains(DOC_QUOTES) ? ALT_DOC_QUOTES : DOC_QUOTES;
|
||||
if (quotes == ALT_DOC_QUOTES) {
|
||||
// ensure there are no problems
|
||||
doc = doc.replaceAll(ALT_DOC_QUOTES, '\\' + ALT_DOC_QUOTES);
|
||||
}
|
||||
printer.print(indent);
|
||||
printer.println(quotes);
|
||||
writeLines(printer, doc.stripTrailing(), indent);
|
||||
printer.print(indent);
|
||||
printer.println(quotes);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Javadoc for this element to the provided printer
|
||||
*
|
||||
* @param printer the printer to write to
|
||||
* @param indent the indentation
|
||||
* @param emptyValue the value to use when there is no documentation
|
||||
*/
|
||||
final void writeJavaDoc(PrintWriter printer, String indent, String emptyValue) {
|
||||
writeJavaDoc(el, printer, indent, emptyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the Javadoc for this element to the provided printer
|
||||
*
|
||||
* @param printer the printer to write to
|
||||
* @param indent the indentation
|
||||
*/
|
||||
final void writeJavaDoc(PrintWriter printer, String indent) {
|
||||
writeJavaDoc(el, printer, indent, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided String Python safe if necessary
|
||||
*
|
||||
* @param value the value to make Python safe
|
||||
* @return the Python safe value
|
||||
*/
|
||||
static String sanitize(String value) {
|
||||
if (PY_KEYWORDS.contains(value)) {
|
||||
return value + "_";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided element name Python safe if necessary
|
||||
*
|
||||
* @param name the name to make Python safe
|
||||
* @return the Python safe name
|
||||
*/
|
||||
static String sanitize(Name name) {
|
||||
return sanitize(name.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided qualified name Python safe if necessary
|
||||
*
|
||||
* @param name the qualified name to make Python safe
|
||||
* @return the Python safe qualified name
|
||||
*/
|
||||
static String sanitizeQualifiedName(String name) {
|
||||
Iterator<String> it = Arrays.stream(name.split("\\."))
|
||||
.map(PythonTypeStubElement::sanitize)
|
||||
.iterator();
|
||||
return String.join(".", (Iterable<String>) () -> it);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided qualified name Python safe if necessary
|
||||
*
|
||||
* @param name the qualified name to make Python safe
|
||||
* @return the Python safe qualified name
|
||||
*/
|
||||
static String sanitizeQualifiedName(QualifiedNameable name) {
|
||||
return sanitizeQualifiedName(name.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided package name Python safe if necessary
|
||||
*
|
||||
* @param pkg the package to make Python safe
|
||||
* @return the Python safe package name
|
||||
*/
|
||||
static String sanitizeQualifiedName(PackageElement pkg) {
|
||||
return sanitizeQualifiedName(pkg.getQualifiedName().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the provided type Python safe if necessary
|
||||
*
|
||||
* @param self the type to become typing.Self if encountered
|
||||
* @param type the type to make Python safe
|
||||
* @param pkg the current package
|
||||
* @return the Python safe type name
|
||||
*/
|
||||
static String sanitize(Element self, TypeMirror type, PackageElement pkg) {
|
||||
return switch (type.getKind()) {
|
||||
case DECLARED -> throw new RuntimeException(
|
||||
"declared types should use the qualified name");
|
||||
case ARRAY -> {
|
||||
TypeMirror component = ((ArrayType) type).getComponentType();
|
||||
yield "jpype.JArray[" + sanitizeQualifiedName(self, component, pkg) + "]";
|
||||
}
|
||||
case BOOLEAN -> "jpype.JBoolean";
|
||||
case BYTE -> "jpype.JByte";
|
||||
case CHAR -> "jpype.JChar";
|
||||
case DOUBLE -> "jpype.JDouble";
|
||||
case FLOAT -> "jpype.JFloat";
|
||||
case INT -> "jpype.JInt";
|
||||
case LONG -> "jpype.JLong";
|
||||
case SHORT -> "jpype.JShort";
|
||||
case TYPEVAR -> type.toString();
|
||||
case WILDCARD -> getWildcardVarName(self, (WildcardType) type, pkg);
|
||||
default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided type is the same as the provided element
|
||||
*
|
||||
* @param self the element of the type to become typing.Self
|
||||
* @param type the type to check
|
||||
* @return true if the inputs represent the same type
|
||||
*/
|
||||
static final boolean isSelfType(Element self, TypeMirror type) {
|
||||
if (self.getKind() == ElementKind.ENUM) {
|
||||
// typing.Self is usually invalid here
|
||||
return false;
|
||||
}
|
||||
if (type instanceof DeclaredType dt) {
|
||||
return self.equals(dt.asElement());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the qualified name for the provided type Python safe if necessary
|
||||
*
|
||||
* @param self the type to become typing.Self if encountered
|
||||
* @param type the type to make Python safe
|
||||
* @return the Python safe qualified type name
|
||||
*/
|
||||
final String sanitizeQualifiedName(Element self, TypeMirror type) {
|
||||
return sanitizeQualifiedName(self, type, pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the qualified name for the provided type Python safe if necessary<p/>
|
||||
*
|
||||
* The provided package is used to check each type and generic components.
|
||||
* If they require a "forward declaration", it is handled accordingly.
|
||||
*
|
||||
* @param self the type to become typing.Self if encountered
|
||||
* @param type the type to make Python safe
|
||||
* @param pkg the current package
|
||||
* @return the Python safe qualified type name
|
||||
*/
|
||||
static final String sanitizeQualifiedName(Element self, TypeMirror type, PackageElement pkg) {
|
||||
if (isSelfType(self, type)) {
|
||||
return "typing.Self";
|
||||
}
|
||||
if (type instanceof DeclaredType dt) {
|
||||
TypeElement el = (TypeElement) dt.asElement();
|
||||
PackageElement typePkg = getPackage(el);
|
||||
|
||||
String name;
|
||||
if (pkg.equals(typePkg)) {
|
||||
name = sanitize(el.getSimpleName());
|
||||
Element parent = el.getEnclosingElement();
|
||||
while (parent instanceof TypeElement parentType) {
|
||||
parent = parent.getEnclosingElement();
|
||||
name = sanitize(parentType.getSimpleName()) + "." + name;
|
||||
}
|
||||
}
|
||||
else {
|
||||
name = sanitizeQualifiedName(el);
|
||||
}
|
||||
|
||||
List<? extends TypeMirror> args = dt.getTypeArguments();
|
||||
if (args.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
Iterable<String> it = () -> args.stream()
|
||||
.map(paramType -> sanitizeQualifiedName(self, paramType, pkg))
|
||||
.iterator();
|
||||
return name + "[" + String.join(", ", it) + "]";
|
||||
}
|
||||
return sanitize(self, type, pkg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is static
|
||||
*
|
||||
* @param el the element to check
|
||||
* @return true if the element is static
|
||||
*/
|
||||
static boolean isStatic(Element el) {
|
||||
return el.getModifiers().contains(Modifier.STATIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is final
|
||||
*
|
||||
* @param el the element to check
|
||||
* @return true if the element is final
|
||||
*/
|
||||
static boolean isFinal(Element el) {
|
||||
return el.getModifiers().contains(Modifier.FINAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is public
|
||||
*
|
||||
* @param el the element to check
|
||||
* @return true if the element is public
|
||||
*/
|
||||
static boolean isPublic(Element el) {
|
||||
return el.getModifiers().contains(Modifier.PUBLIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided element is protected
|
||||
*
|
||||
* @param el the element to check
|
||||
* @return true if the element is protected
|
||||
*/
|
||||
static boolean isProtected(Element el) {
|
||||
return el.getModifiers().contains(Modifier.PROTECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the provided indentation by one level
|
||||
*
|
||||
* @param indent the indentation
|
||||
* @return the new indentation
|
||||
*/
|
||||
static String indent(String indent) {
|
||||
return indent + PY_INDENT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decreases the provided indentation by one level
|
||||
*
|
||||
* @param indent the indentation
|
||||
* @return the new indentation
|
||||
*/
|
||||
static String deindent(String indent) {
|
||||
return indent.substring(0, indent.length() - PY_INDENT.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name for a wildcard type if possible
|
||||
*
|
||||
* @param self the type to become typing.Self if encountered
|
||||
* @param type the wildcard type
|
||||
* @param pkg the current package
|
||||
* @return the determined type name if possible otherwise typing.Any
|
||||
*/
|
||||
private static String getWildcardVarName(Element self, WildcardType type, PackageElement pkg) {
|
||||
TypeMirror base = type.getExtendsBound();
|
||||
if (base == null) {
|
||||
base = type.getSuperBound();
|
||||
}
|
||||
if (base != null) {
|
||||
return sanitizeQualifiedName(self, base, pkg);
|
||||
}
|
||||
return "typing.Any";
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the lines to the printer with the provided intentation
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param lines the lines to write
|
||||
* @param indent the indentation to use
|
||||
*/
|
||||
private static void writeLines(PrintWriter printer, String lines, String indent) {
|
||||
lines.lines().forEach((line) -> {
|
||||
printer.print(indent);
|
||||
printer.println(line);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,499 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.Modifier;
|
||||
import javax.lang.model.element.QualifiedNameable;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.TypeParameterElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.ArrayType;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.ExecutableType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
/**
|
||||
* {@link PythonTypeStubElement} for a method
|
||||
*/
|
||||
final class PythonTypeStubMethod extends PythonTypeStubElement<ExecutableElement>
|
||||
implements Comparable<PythonTypeStubMethod> {
|
||||
|
||||
private static final String EMPTY_DOCS = "..." + System.lineSeparator();
|
||||
|
||||
private static final Map<String, String> AUTO_CONVERSIONS = new HashMap<>(
|
||||
Map.ofEntries(
|
||||
Map.entry("java.lang.Boolean", "typing.Union[java.lang.Boolean, bool]"),
|
||||
Map.entry("java.lang.Byte", "typing.Union[java.lang.Byte, int]"),
|
||||
Map.entry("java.lang.Character", "typing.Union[java.lang.Character, int, str]"),
|
||||
Map.entry("java.lang.Double", "typing.Union[java.lang.Double, float]"),
|
||||
Map.entry("java.lang.Float", "typing.Union[java.lang.Float, float]"),
|
||||
Map.entry("java.lang.Integer", "typing.Union[java.lang.Integer, int]"),
|
||||
Map.entry("java.lang.Long", "typing.Union[java.lang.Long, int]"),
|
||||
Map.entry("java.lang.Short", "typing.Union[java.lang.Short, int]"),
|
||||
Map.entry("java.lang.String", "typing.Union[java.lang.String, str]"),
|
||||
Map.entry("java.io.File", "jpype.protocol.SupportsPath"),
|
||||
Map.entry("java.nio.file.Path", "jpype.protocol.SupportsPath"),
|
||||
Map.entry("java.lang.Iterable", "collections.abc.Sequence"),
|
||||
Map.entry("java.util.Collection", "collections.abc.Sequence"),
|
||||
Map.entry("java.util.Map", "collections.abc.Mapping"),
|
||||
Map.entry("java.time.Instant", "datetime.datetime"),
|
||||
Map.entry("java.sql.Time", "datetime.time"),
|
||||
Map.entry("java.sql.Date", "datetime.date"),
|
||||
Map.entry("java.sql.Timestamp", "datetime.datetime"),
|
||||
Map.entry("java.math.BigDecimal", "decimal.Decimal")));
|
||||
|
||||
// FIXME: list and set aren't automatically converted to java.util.List and java.util.Set :(
|
||||
// if wanted they could be setup to be converted automatically by pyhidra
|
||||
// however, when passed as a parameter and modified, the original underlyng python container
|
||||
// wouldn't be modified. To make it work as expected, a python implementation for
|
||||
// java.util.List and java.util.Set would need to be created using jpype.JImplements,
|
||||
// that would wrap the list/set before passing it to Java instead of copying the contents
|
||||
// into a Java List/Set.
|
||||
|
||||
private static final Map<String, String> RESULT_CONVERSIONS = new HashMap<>(
|
||||
Map.of(
|
||||
"java.lang.Boolean", "bool",
|
||||
"java.lang.Byte", "int",
|
||||
"java.lang.Character", "str",
|
||||
"java.lang.Double", "float",
|
||||
"java.lang.Float", "float",
|
||||
"java.lang.Integer", "int",
|
||||
"java.lang.Long", "int",
|
||||
"java.lang.Short", "int",
|
||||
"java.lang.String", "str"));
|
||||
|
||||
private final PythonTypeStubType parent;
|
||||
private final boolean filterSelf;
|
||||
List<String> typevars;
|
||||
Set<TypeElement> imports;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PythonTypeStubMethod}
|
||||
*
|
||||
* @param parent the type containing this method
|
||||
* @param el the element for this method
|
||||
*/
|
||||
PythonTypeStubMethod(PythonTypeStubType parent, ExecutableElement el) {
|
||||
this(parent, el, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link PythonTypeStubMethod}
|
||||
*
|
||||
* @param parent the type containing this method
|
||||
* @param el the element for this method
|
||||
* @param filterSelf true if the self parameter should be filtered
|
||||
*/
|
||||
PythonTypeStubMethod(PythonTypeStubType parent, ExecutableElement el, boolean filterSelf) {
|
||||
super(parent.doclet, el);
|
||||
this.parent = parent;
|
||||
this.filterSelf = filterSelf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the method and prints it to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the indentation
|
||||
* @param overload true if the overload annotation should be applied
|
||||
*/
|
||||
void process(PrintWriter printer, String indent, boolean overload) {
|
||||
String name = sanitize(getName());
|
||||
Set<Modifier> modifiers = el.getModifiers();
|
||||
boolean isStatic = modifiers.contains(Modifier.STATIC);
|
||||
|
||||
if (name.equals("<init>")) {
|
||||
name = "__init__";
|
||||
}
|
||||
|
||||
printer.print(indent);
|
||||
if (isStatic) {
|
||||
printer.println("@staticmethod");
|
||||
printer.print(indent);
|
||||
}
|
||||
|
||||
if (overload) {
|
||||
printer.println("@typing.overload");
|
||||
printer.print(indent);
|
||||
}
|
||||
|
||||
if (doclet.isDeprecated(el)) {
|
||||
String msg = doclet.getDeprecatedMessage(el);
|
||||
if (msg != null) {
|
||||
// a message is required
|
||||
// if one is not present, don't apply it
|
||||
printer.print("@deprecated(");
|
||||
printer.print(msg);
|
||||
printer.println(')');
|
||||
printer.print(indent);
|
||||
}
|
||||
}
|
||||
|
||||
printer.print("def ");
|
||||
printer.print(name);
|
||||
|
||||
printSignature(printer, filterSelf || isStatic);
|
||||
|
||||
printer.println(":");
|
||||
indent += PY_INDENT;
|
||||
writeJavaDoc(el, printer, indent, EMPTY_DOCS);
|
||||
printer.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of all TypeVars needed by this method
|
||||
*
|
||||
* @return a collection of all needed TypeVars
|
||||
*/
|
||||
Collection<String> getTypeVars() {
|
||||
if (typevars != null) {
|
||||
return typevars;
|
||||
}
|
||||
|
||||
List<? extends TypeParameterElement> params = el.getTypeParameters();
|
||||
typevars = new ArrayList<>(params.size());
|
||||
for (TypeParameterElement param : params) {
|
||||
typevars.add(param.getSimpleName().toString());
|
||||
}
|
||||
return typevars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of all type that need to be imported for this method
|
||||
*
|
||||
* @return a collection of types to import
|
||||
*/
|
||||
Collection<TypeElement> getImportedTypes() {
|
||||
if (imports != null) {
|
||||
return imports;
|
||||
}
|
||||
|
||||
List<? extends VariableElement> parameters = el.getParameters();
|
||||
TypeMirror resType = el.getReturnType();
|
||||
|
||||
// make the set big enough for all paramters and the return type
|
||||
imports = new HashSet<>(parameters.size() + 1);
|
||||
|
||||
if (resType instanceof DeclaredType dt) {
|
||||
imports.add((TypeElement) dt.asElement());
|
||||
}
|
||||
|
||||
for (VariableElement param : parameters) {
|
||||
if (param.asType() instanceof DeclaredType dt) {
|
||||
imports.add((TypeElement) dt.asElement());
|
||||
}
|
||||
}
|
||||
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the result type to the Python equivalent type if applicable
|
||||
*
|
||||
* @param type the result type
|
||||
* @return the Python equivalent type or null if there is no equivalent type
|
||||
*/
|
||||
static String convertResultType(TypeMirror type) {
|
||||
if (type.getKind().isPrimitive()) {
|
||||
return switch (type.getKind()) {
|
||||
case BOOLEAN -> "bool";
|
||||
case BYTE -> "int";
|
||||
case CHAR -> "str";
|
||||
case DOUBLE -> "float";
|
||||
case FLOAT -> "float";
|
||||
case INT -> "int";
|
||||
case LONG -> "int";
|
||||
case SHORT -> "int";
|
||||
default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
|
||||
};
|
||||
}
|
||||
|
||||
if (type instanceof DeclaredType dt) {
|
||||
Element element = dt.asElement();
|
||||
if (element instanceof QualifiedNameable nameable) {
|
||||
return RESULT_CONVERSIONS.get(nameable.getQualifiedName().toString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this method is a candidate for a Python property
|
||||
*
|
||||
* @return true if this method may be a Python property
|
||||
*/
|
||||
boolean isProperty() {
|
||||
if (isStatic(el)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<? extends VariableElement> params = el.getParameters();
|
||||
if (params.size() > 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String name = getName();
|
||||
TypeKind resultKind = getReturnType().getKind();
|
||||
try {
|
||||
if (name.startsWith("get")) {
|
||||
return Character.isUpperCase(name.charAt(3)) && resultKind != TypeKind.VOID;
|
||||
}
|
||||
if (name.startsWith("is")) {
|
||||
return Character.isUpperCase(name.charAt(2)) && resultKind != TypeKind.VOID;
|
||||
}
|
||||
if (name.startsWith("set")) {
|
||||
if (params.size() != 1) {
|
||||
return false;
|
||||
}
|
||||
return Character.isUpperCase(name.charAt(3)) && resultKind == TypeKind.VOID;
|
||||
}
|
||||
}
|
||||
catch (IndexOutOfBoundsException e) {
|
||||
// name check failed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this method to its Python property form
|
||||
*
|
||||
* @return this method as a Python property
|
||||
*/
|
||||
PropertyMethod asProperty() {
|
||||
return new PropertyMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the Python equivalent method signature to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param isStatic true if this method is a static method
|
||||
*/
|
||||
private void printSignature(PrintWriter printer, boolean isStatic) {
|
||||
List<String> names = getParameterNames();
|
||||
List<? extends TypeMirror> types = getParameterTypes();
|
||||
StringBuilder args = new StringBuilder();
|
||||
|
||||
if (!isStatic) {
|
||||
args.append("self");
|
||||
}
|
||||
|
||||
for (int i = 0; i < names.size(); i++) {
|
||||
if (i != 0 || !isStatic) {
|
||||
args.append(", ");
|
||||
}
|
||||
if (el.isVarArgs() && i == names.size() - 1) {
|
||||
ArrayType type = (ArrayType) types.get(i);
|
||||
String arg = convertParam(names.get(i), type.getComponentType());
|
||||
args.append('*' + arg);
|
||||
}
|
||||
else {
|
||||
args.append(convertParam(names.get(i), types.get(i)));
|
||||
}
|
||||
}
|
||||
|
||||
printer.print("(");
|
||||
printer.print(args);
|
||||
printer.print(")");
|
||||
|
||||
TypeMirror res = el.getReturnType();
|
||||
if (res.getKind() != TypeKind.VOID) {
|
||||
printer.print(" -> ");
|
||||
String convertedType = convertResultType(res);
|
||||
if (convertedType != null) {
|
||||
printer.print(convertedType);
|
||||
}
|
||||
else {
|
||||
printer.print(getTypeString(parent.el, res));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the property name for this method if applicable
|
||||
*
|
||||
* @return the property name or null
|
||||
*/
|
||||
private String getPropertyName() {
|
||||
String name = getName();
|
||||
if (name.startsWith("get") || name.startsWith("set")) {
|
||||
return Character.toLowerCase(name.charAt(3)) + name.substring(4);
|
||||
}
|
||||
if (name.startsWith("is")) {
|
||||
return Character.toLowerCase(name.charAt(2)) + name.substring(3);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the parameter types
|
||||
*
|
||||
* @return the list of parameter types
|
||||
*/
|
||||
private List<? extends TypeMirror> getParameterTypes() {
|
||||
return ((ExecutableType) el.asType()).getParameterTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the Python safe parameter names
|
||||
*
|
||||
* @return the list of parameter names
|
||||
*/
|
||||
private List<String> getParameterNames() {
|
||||
List<? extends VariableElement> params = el.getParameters();
|
||||
List<String> names = new ArrayList<>(params.size());
|
||||
for (VariableElement param : params) {
|
||||
String name = sanitize(param.getSimpleName());
|
||||
if (name.equals("self")) {
|
||||
name = "self_";
|
||||
}
|
||||
names.add(name);
|
||||
}
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the return type
|
||||
*
|
||||
* @return the return type
|
||||
*/
|
||||
private TypeMirror getReturnType() {
|
||||
return el.getReturnType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided parameter type to a typing.Union of all the allowed types
|
||||
*
|
||||
* @param name the parameter name
|
||||
* @param type the parameter type
|
||||
* @return the parameter and its type
|
||||
*/
|
||||
private String convertParam(String name, TypeMirror type) {
|
||||
String convertedType = convertParamType(type);
|
||||
if (convertedType != null) {
|
||||
return name + ": " + convertedType;
|
||||
}
|
||||
return name + ": " + getTypeString(parent.el, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided parameter type to a typing.Union of all the allowed types
|
||||
*
|
||||
* @param type the parameter type
|
||||
* @return the converted type
|
||||
*/
|
||||
private static String convertParamType(TypeMirror type) {
|
||||
if (type.getKind().isPrimitive()) {
|
||||
return switch (type.getKind()) {
|
||||
case BOOLEAN -> "typing.Union[jpype.JBoolean, bool]";
|
||||
case BYTE -> "typing.Union[jpype.JByte, int]";
|
||||
case CHAR -> "typing.Union[jpype.JChar, int, str]";
|
||||
case DOUBLE -> "typing.Union[jpype.JDouble, float]";
|
||||
case FLOAT -> "typing.Union[jpype.JFloat, float]";
|
||||
case INT -> "typing.Union[jpype.JInt, int]";
|
||||
case LONG -> "typing.Union[jpype.JLong, int]";
|
||||
case SHORT -> "typing.Union[jpype.JShort, int]";
|
||||
default -> throw new RuntimeException("unexpected TypeKind " + type.getKind());
|
||||
};
|
||||
}
|
||||
|
||||
if (type instanceof DeclaredType dt) {
|
||||
Element element = dt.asElement();
|
||||
if (element instanceof QualifiedNameable nameable) {
|
||||
return AUTO_CONVERSIONS.get(nameable.getQualifiedName().toString());
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating a Python property.<p/>
|
||||
*
|
||||
* This class only represents one part of a complete Python property.
|
||||
*/
|
||||
class PropertyMethod {
|
||||
|
||||
/**
|
||||
* Gets the name for this property
|
||||
*
|
||||
* @return the property name
|
||||
*/
|
||||
String getName() {
|
||||
return sanitize(getPropertyName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this property is a getter
|
||||
*
|
||||
* @return true if this property is a getter
|
||||
*/
|
||||
boolean isGetter() {
|
||||
return el.getReturnType().getKind() != TypeKind.VOID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this property is a setter
|
||||
*
|
||||
* @return true if this property is a setter
|
||||
*/
|
||||
boolean isSetter() {
|
||||
return el.getReturnType().getKind() == TypeKind.VOID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type for this property
|
||||
*
|
||||
* @return the property type
|
||||
*/
|
||||
TypeMirror getType() {
|
||||
TypeMirror type;
|
||||
if (isGetter()) {
|
||||
type = el.getReturnType();
|
||||
}
|
||||
else {
|
||||
type = getParameterTypes().get(0);
|
||||
}
|
||||
try {
|
||||
return doclet.getTypeUtils().unboxedType(type);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
// not boxed
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this property and the other provided property form a pair
|
||||
*
|
||||
* @param other the other property
|
||||
* @return true if the two properties form a pair
|
||||
*/
|
||||
boolean isPair(PropertyMethod other) {
|
||||
if (isGetter() && other.isGetter()) {
|
||||
return false;
|
||||
}
|
||||
if (isSetter() && other.isSetter()) {
|
||||
return false;
|
||||
}
|
||||
if (!getName().equals(other.getName())) {
|
||||
return false;
|
||||
}
|
||||
return doclet.getTypeUtils().isSameType(getType(), other.getType());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(PythonTypeStubMethod other) {
|
||||
return getName().compareTo(other.getName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
/**
|
||||
* {@link PythonTypeStubElement} for a nested type
|
||||
*/
|
||||
final class PythonTypeStubNestedType extends PythonTypeStubType {
|
||||
|
||||
// while it is possible to create a pseudo sub module to
|
||||
// make static nested classes and enum values individually
|
||||
// importable during type checking, it's not worth the effort
|
||||
|
||||
/**
|
||||
* Creates a new {@link PythonTypeStubNestedType}
|
||||
*
|
||||
* @param pkg the package containing this type
|
||||
* @param el the element for this type
|
||||
*/
|
||||
PythonTypeStubNestedType(PythonTypeStubPackage pkg, TypeElement el) {
|
||||
super(pkg, el);
|
||||
}
|
||||
|
||||
@Override
|
||||
void process(PrintWriter printer, String indent) {
|
||||
printClass(printer, indent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.PackageElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
|
||||
/**
|
||||
* {@link PythonTypeStubElement} for a package<p/>
|
||||
*
|
||||
* This will process all visible classes, interfaces, handle necessary imports
|
||||
* and create the __init__.pyi file.
|
||||
*/
|
||||
final class PythonTypeStubPackage extends PythonTypeStubElement<PackageElement> {
|
||||
|
||||
private String packageName;
|
||||
private File path;
|
||||
private List<PythonTypeStubType> types;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PythonTypeStubPackage}
|
||||
*
|
||||
* @param doclet the current doclet
|
||||
* @param el the element for this package
|
||||
*/
|
||||
PythonTypeStubPackage(PythonTypeStubDoclet doclet, PackageElement el) {
|
||||
super(doclet, el);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the TypeVars needed by the types in this package
|
||||
*
|
||||
* @return a list of all the needed TypeVars
|
||||
*/
|
||||
List<String> getTypeVars() {
|
||||
Set<String> typevars = new HashSet<>();
|
||||
for (PythonTypeStubType type : getTypes()) {
|
||||
typevars.addAll(type.getTypeVars());
|
||||
}
|
||||
List<String> res = new ArrayList<>(typevars);
|
||||
res.sort(null);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of all the imported types needed by the types in this package
|
||||
*
|
||||
* @return a collection of all the imported types
|
||||
*/
|
||||
Collection<TypeElement> getImportedTypes() {
|
||||
Set<TypeElement> imported = new HashSet<>();
|
||||
for (PythonTypeStubType type : getTypes()) {
|
||||
imported.addAll(type.getImportedTypes());
|
||||
}
|
||||
return imported;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Python safe, fully qualified name for this package
|
||||
*
|
||||
* @return the qualified package name
|
||||
*/
|
||||
String getPackageName() {
|
||||
if (packageName == null) {
|
||||
packageName = sanitizeQualifiedName(el.getQualifiedName().toString());
|
||||
}
|
||||
return packageName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes this package and its contents to create a __init__.pyi file
|
||||
*/
|
||||
void process() {
|
||||
doclet.addProcessedPackage(el);
|
||||
getPath().mkdirs();
|
||||
File stub = new File(path, "__init__.pyi");
|
||||
try (PrintWriter printer = new PrintWriter(new FileWriter(stub))) {
|
||||
process(printer, "");
|
||||
}
|
||||
catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the types declared in this package
|
||||
*
|
||||
* @return a list of all specified types
|
||||
*/
|
||||
List<PythonTypeStubType> getTypes() {
|
||||
// NOTE: do ALL SPECIFIED TYPES
|
||||
// if it is not public, it will be decorated with @typing.type_check_only
|
||||
// this prevents errors during typechecking from having a class with a base
|
||||
// class that doesn't have public visibility
|
||||
if (types != null) {
|
||||
return types;
|
||||
}
|
||||
types = new ArrayList<>();
|
||||
for (Element child : el.getEnclosedElements()) {
|
||||
switch (child.getKind()) {
|
||||
case CLASS:
|
||||
case INTERFACE:
|
||||
case ENUM:
|
||||
case RECORD:
|
||||
if (!doclet.isSpecified(child)) {
|
||||
continue;
|
||||
}
|
||||
types.add(new PythonTypeStubType(this, (TypeElement) child));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the contents of this package and write the results to the provided printer
|
||||
*
|
||||
* @param printer the printer to write to
|
||||
* @param indent the current indentation
|
||||
*/
|
||||
private void process(PrintWriter printer, String indent) {
|
||||
writeJavaDoc(printer, indent, "");
|
||||
printer.println("import collections.abc");
|
||||
printer.println("import datetime");
|
||||
printer.println("import typing");
|
||||
printer.println("from warnings import deprecated # type: ignore");
|
||||
printer.println();
|
||||
printer.println("import jpype # type: ignore");
|
||||
printer.println("import jpype.protocol # type: ignore");
|
||||
printer.println();
|
||||
doclet.printImports(printer, getImportedPackages());
|
||||
printer.println();
|
||||
printer.println();
|
||||
printTypeVars(printer);
|
||||
Set<String> exports = new LinkedHashSet<>();
|
||||
for (PythonTypeStubType type : getTypes()) {
|
||||
processType(printer, indent, type);
|
||||
exports.add('"' + type.getName() + '"');
|
||||
}
|
||||
printer.println();
|
||||
|
||||
// create the __all__ variable to prevent our imports and TypeVars from being
|
||||
// imported when "from {getPackageName()} import *" is used
|
||||
printer.print("__all__ = [");
|
||||
printer.print(String.join(", ", exports));
|
||||
printer.println("]");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the output directory for this package
|
||||
*
|
||||
* @return the output directory
|
||||
*/
|
||||
private File getPath() {
|
||||
if (path == null) {
|
||||
String name = getPackageName();
|
||||
int index = name.indexOf('.');
|
||||
if (index != -1) {
|
||||
name = name.substring(0, index) + "-stubs" + name.substring(index);
|
||||
}
|
||||
else {
|
||||
name += "-stubs";
|
||||
}
|
||||
path = new File(doclet.getDestDir(), name.replace('.', '/'));
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of all imported packages
|
||||
*
|
||||
* @return a collection of all imported packages
|
||||
*/
|
||||
private Collection<PackageElement> getImportedPackages() {
|
||||
Set<PackageElement> packages = new HashSet<>();
|
||||
for (TypeElement element : getImportedTypes()) {
|
||||
if (isNestedType(element)) {
|
||||
// don't import types declared in this file
|
||||
continue;
|
||||
}
|
||||
|
||||
PackageElement importedPkg = getPackage(element);
|
||||
if (importedPkg == null || el.equals(importedPkg)) {
|
||||
continue;
|
||||
}
|
||||
packages.add(importedPkg);
|
||||
}
|
||||
|
||||
List<PackageElement> res = new ArrayList<>(packages);
|
||||
res.sort(PythonTypeStubElement::compareQualifiedNameable);
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the provided type and write it to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the current indentation
|
||||
* @param type the type
|
||||
*/
|
||||
private void processType(PrintWriter printer, String indent, PythonTypeStubType type) {
|
||||
type.process(printer, indent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided type is a nested type
|
||||
*
|
||||
* @param element the type element to check
|
||||
* @return true if the type is declared within another class
|
||||
*/
|
||||
private static boolean isNestedType(TypeElement element) {
|
||||
return element.getEnclosingElement() instanceof TypeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints all the typevars to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
*/
|
||||
private void printTypeVars(PrintWriter printer) {
|
||||
List<String> allTypeVars = getTypeVars();
|
||||
for (String generic : allTypeVars) {
|
||||
printer.println(generic + " = typing.TypeVar(\"" + generic + "\")");
|
||||
}
|
||||
if (!allTypeVars.isEmpty()) {
|
||||
printer.println();
|
||||
printer.println();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,715 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.TypeParameterElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.lang.model.type.DeclaredType;
|
||||
import javax.lang.model.type.TypeKind;
|
||||
import javax.lang.model.type.TypeMirror;
|
||||
|
||||
import com.sun.source.doctree.DocTree;
|
||||
|
||||
/**
|
||||
* {@link PythonTypeStubElement} for a declared type
|
||||
*/
|
||||
class PythonTypeStubType extends PythonTypeStubElement<TypeElement> {
|
||||
|
||||
private static final String OBJECT_NAME = Object.class.getName();
|
||||
private static final Map<String, String> GENERIC_CUSTOMIZERS = new HashMap<>(Map.ofEntries(
|
||||
Map.entry("java.lang.Iterable", "collections.abc.Iterable"),
|
||||
Map.entry("java.util.Collection", "collections.abc.Collection"),
|
||||
Map.entry("java.util.List", "list"),
|
||||
Map.entry("java.util.Map", "dict"),
|
||||
Map.entry("java.util.Set", "set"),
|
||||
Map.entry("java.util.Map.Entry", "tuple"),
|
||||
Map.entry("java.util.Iterator", "collections.abc.Iterator"),
|
||||
Map.entry("java.util.Enumeration", "collections.abc.Iterator")));
|
||||
|
||||
private final PythonTypeStubPackage pkg;
|
||||
private Set<TypeElement> imports;
|
||||
private Set<String> typevars;
|
||||
private List<PythonTypeStubNestedType> nestedTypes;
|
||||
private List<VariableElement> fields;
|
||||
private List<PythonTypeStubMethod> methods;
|
||||
private List<Property> properties;
|
||||
private Set<String> fieldNames;
|
||||
private Set<String> methodNames;
|
||||
|
||||
/**
|
||||
* Creates a new {@link PythonTypeStubType}
|
||||
*
|
||||
* @param pkg the package containing this type
|
||||
* @param el the element for this type
|
||||
*/
|
||||
PythonTypeStubType(PythonTypeStubPackage pkg, TypeElement el) {
|
||||
super(pkg.doclet, pkg.el, el);
|
||||
this.pkg = pkg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the current type and write it to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the indentation
|
||||
*/
|
||||
void process(PrintWriter printer, String indent) {
|
||||
printClass(printer, indent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of all the TypeVars used by this type
|
||||
*
|
||||
* @return a set of all used TypeVars
|
||||
*/
|
||||
Set<String> getTypeVars() {
|
||||
if (typevars != null) {
|
||||
return typevars;
|
||||
}
|
||||
List<? extends TypeParameterElement> params = el.getTypeParameters();
|
||||
typevars = new HashSet<>();
|
||||
for (TypeParameterElement param : params) {
|
||||
typevars.add(param.getSimpleName().toString());
|
||||
}
|
||||
for (PythonTypeStubNestedType nested : getNestedTypes()) {
|
||||
typevars.addAll(nested.getTypeVars());
|
||||
}
|
||||
for (PythonTypeStubMethod method : getMethods()) {
|
||||
typevars.addAll(method.getTypeVars());
|
||||
}
|
||||
return typevars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a collection of all the imported types used by this type
|
||||
*
|
||||
* @return a collection of all imported types
|
||||
*/
|
||||
final Collection<TypeElement> getImportedTypes() {
|
||||
if (imports != null) {
|
||||
return imports;
|
||||
}
|
||||
imports = new HashSet<>();
|
||||
TypeMirror base = el.getSuperclass();
|
||||
if (base instanceof DeclaredType dt) {
|
||||
imports.add((TypeElement) dt.asElement());
|
||||
}
|
||||
for (TypeMirror iface : el.getInterfaces()) {
|
||||
if (iface instanceof DeclaredType dt) {
|
||||
imports.add((TypeElement) dt.asElement());
|
||||
}
|
||||
}
|
||||
for (PythonTypeStubNestedType nested : getNestedTypes()) {
|
||||
imports.addAll(nested.getImportedTypes());
|
||||
}
|
||||
for (VariableElement field : getFields()) {
|
||||
TypeMirror fieldType = field.asType();
|
||||
if (fieldType instanceof DeclaredType dt) {
|
||||
imports.add((TypeElement) dt.asElement());
|
||||
}
|
||||
}
|
||||
for (PythonTypeStubMethod method : getMethods()) {
|
||||
imports.addAll(method.getImportedTypes());
|
||||
}
|
||||
return imports;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the nested types declared in this type
|
||||
*
|
||||
* @return a list of all nested types
|
||||
*/
|
||||
final List<PythonTypeStubNestedType> getNestedTypes() {
|
||||
if (nestedTypes != null) {
|
||||
return nestedTypes;
|
||||
}
|
||||
nestedTypes = new ArrayList<>();
|
||||
for (Element child : el.getEnclosedElements()) {
|
||||
if (child instanceof TypeElement type) {
|
||||
nestedTypes.add(new PythonTypeStubNestedType(pkg, type));
|
||||
}
|
||||
}
|
||||
return nestedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the public fields in this type
|
||||
*
|
||||
* @return a list of all public fields
|
||||
*/
|
||||
final List<VariableElement> getFields() {
|
||||
return getFields(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all the visible fields in this type
|
||||
*
|
||||
* @param protectedScope true to include protected fields
|
||||
* @return a list of all visible fields
|
||||
*/
|
||||
final List<VariableElement> getFields(boolean protectedScope) {
|
||||
if (fields != null) {
|
||||
return fields;
|
||||
}
|
||||
fields = new ArrayList<>();
|
||||
for (Element child : el.getEnclosedElements()) {
|
||||
switch (child.getKind()) {
|
||||
case ENUM_CONSTANT:
|
||||
case FIELD:
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
if (!isVisible(child, protectedScope)) {
|
||||
continue;
|
||||
}
|
||||
fields.add((VariableElement) child);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all public methods and constructors in this type
|
||||
*
|
||||
* @return a list of all public methods
|
||||
*/
|
||||
final List<PythonTypeStubMethod> getMethods() {
|
||||
return getMethods(false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of all visible methods in this type
|
||||
*
|
||||
* @param protectedScope true to include protected methods
|
||||
* @param filterConstructor true to filter constructors
|
||||
* @return a list of visible methods
|
||||
*/
|
||||
final List<PythonTypeStubMethod> getMethods(boolean protectedScope, boolean filterConstructor) {
|
||||
if (methods != null) {
|
||||
return methods;
|
||||
}
|
||||
methods = new ArrayList<>();
|
||||
for (Element child : el.getEnclosedElements()) {
|
||||
switch (child.getKind()) {
|
||||
case CONSTRUCTOR:
|
||||
if (filterConstructor) {
|
||||
continue;
|
||||
}
|
||||
case METHOD:
|
||||
if (!isVisible(child, protectedScope)) {
|
||||
continue;
|
||||
}
|
||||
if (isUndocumentedOverride(child)) {
|
||||
continue;
|
||||
}
|
||||
methods.add(new PythonTypeStubMethod(this, (ExecutableElement) child,
|
||||
filterConstructor));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
// apparently overloads must come one after another
|
||||
// therefore this must be sorted
|
||||
methods.sort(null);
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the provided method needs the typing.overload decorator
|
||||
*
|
||||
* @param methods the list of methods
|
||||
* @param it the current iterator
|
||||
* @param method the method to check
|
||||
* @return true if typing.overload should be applied
|
||||
*/
|
||||
static boolean isOverload(List<PythonTypeStubMethod> methods,
|
||||
ListIterator<PythonTypeStubMethod> it, PythonTypeStubMethod method) {
|
||||
if (it.hasNext()) {
|
||||
if (methods.get(it.nextIndex()).getName().equals(method.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
int index = it.previousIndex();
|
||||
if (index >= 1) {
|
||||
// the previous index is actually the index of the method parameter
|
||||
if (methods.get(index - 1).getName().equals(method.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the Python class definition for this type to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the current indentation
|
||||
*/
|
||||
final void printClass(PrintWriter printer, String indent) {
|
||||
printClassDefinition(printer, indent);
|
||||
indent = indent(indent);
|
||||
for (PythonTypeStubNestedType nested : getNestedTypes()) {
|
||||
nested.process(printer, indent);
|
||||
}
|
||||
for (VariableElement field : getFields()) {
|
||||
printField(field, printer, indent, isStatic(field));
|
||||
}
|
||||
if (!getFields().isEmpty()) {
|
||||
printer.println();
|
||||
}
|
||||
ListIterator<PythonTypeStubMethod> methodIterator = getMethods().listIterator();
|
||||
while (methodIterator.hasNext()) {
|
||||
PythonTypeStubMethod method = methodIterator.next();
|
||||
boolean overload = isOverload(getMethods(), methodIterator, method);
|
||||
method.process(printer, indent, overload);
|
||||
}
|
||||
if (!doclet.isUsingPythonProperties()) {
|
||||
printer.println();
|
||||
return;
|
||||
}
|
||||
for (Property property : getProperties()) {
|
||||
property.process(printer, indent);
|
||||
}
|
||||
printer.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the provided field to the provided printer
|
||||
*
|
||||
* @param field the field to print
|
||||
* @param printer the printer
|
||||
* @param indent the indentation
|
||||
* @param isStatic true if the field is static
|
||||
*/
|
||||
void printField(VariableElement field, PrintWriter printer, String indent, boolean isStatic) {
|
||||
String name = sanitize(field.getSimpleName());
|
||||
printer.print(indent);
|
||||
printer.print(name);
|
||||
|
||||
String value = getConstantValue(field);
|
||||
if (value != null) {
|
||||
// constants are always static final
|
||||
printer.print(": typing.Final = ");
|
||||
printer.println(value);
|
||||
}
|
||||
else {
|
||||
TypeMirror type = field.asType();
|
||||
printer.print(": ");
|
||||
String sanitizedType = getTypeString(el, type);
|
||||
|
||||
// only one of these may be applied
|
||||
// prefer Final over ClassVar
|
||||
if (isFinal(field)) {
|
||||
sanitizedType = applyFinal(sanitizedType);
|
||||
}
|
||||
else if (isStatic) {
|
||||
sanitizedType = applyClassVar(sanitizedType);
|
||||
}
|
||||
|
||||
printer.print(sanitizedType);
|
||||
printer.println();
|
||||
}
|
||||
|
||||
if (writeJavaDoc(field, printer, indent, "")) {
|
||||
printer.println();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the provided type in typing.ClassVar
|
||||
*
|
||||
* @param type the type to wrap
|
||||
* @return the wrapped type
|
||||
*/
|
||||
private static String applyClassVar(String type) {
|
||||
if (!type.isEmpty()) {
|
||||
return "typing.ClassVar[" + type + ']';
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps the provided type in typing.Final
|
||||
*
|
||||
* @param type the type to wrap
|
||||
* @return the wrapped type
|
||||
*/
|
||||
private static String applyFinal(String type) {
|
||||
if (!type.isEmpty()) {
|
||||
return "typing.Final[" + type + ']';
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of TypeVars for only this type
|
||||
*
|
||||
* @return the list of TypeVars for this type
|
||||
*/
|
||||
private List<String> getClassTypeVars() {
|
||||
List<? extends TypeParameterElement> params = el.getTypeParameters();
|
||||
List<String> res = new ArrayList<>(params.size());
|
||||
for (TypeParameterElement param : params) {
|
||||
res.add(param.getSimpleName().toString());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of the Python properties to be created for this type
|
||||
*
|
||||
* @return the list of Python properties
|
||||
*/
|
||||
private List<Property> getProperties() {
|
||||
if (properties != null) {
|
||||
return properties;
|
||||
}
|
||||
properties = getMethods()
|
||||
.stream()
|
||||
.filter(PythonTypeStubMethod::isProperty)
|
||||
.map(PythonTypeStubMethod::asProperty)
|
||||
.collect(Collectors.groupingBy(PythonTypeStubMethod.PropertyMethod::getName))
|
||||
.values()
|
||||
.stream()
|
||||
.map(this::mergeProperties)
|
||||
.flatMap(Optional::stream)
|
||||
.collect(Collectors.toList());
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the provided pairs into one Python property
|
||||
*
|
||||
* @param pairs the property pairs
|
||||
* @return an optional Python property
|
||||
*/
|
||||
private Optional<Property> mergeProperties(List<PythonTypeStubMethod.PropertyMethod> pairs) {
|
||||
Property res = new Property();
|
||||
if (pairs.size() == 1) {
|
||||
PythonTypeStubMethod.PropertyMethod p = pairs.get(0);
|
||||
if (p.isGetter()) {
|
||||
res.getter = p;
|
||||
}
|
||||
else {
|
||||
res.setter = p;
|
||||
}
|
||||
return Optional.of(res);
|
||||
}
|
||||
PythonTypeStubMethod.PropertyMethod getter = pairs.stream()
|
||||
.filter(PythonTypeStubMethod.PropertyMethod::isGetter)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (getter != null) {
|
||||
// go through all remaining methods and take the first matching pair
|
||||
// it does not matter if one is a boxed primitive and the other is
|
||||
// unboxed because the JavaProperty will use the primitive type anyway
|
||||
PythonTypeStubMethod.PropertyMethod setter = pairs.stream()
|
||||
.filter(PythonTypeStubMethod.PropertyMethod::isSetter)
|
||||
.filter(getter::isPair)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
res.getter = getter;
|
||||
res.setter = setter;
|
||||
return Optional.of(res);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of the public method names for this type
|
||||
*
|
||||
* @return the set of public method names
|
||||
*/
|
||||
private Set<String> getMethodNames() {
|
||||
if (methodNames != null) {
|
||||
return methodNames;
|
||||
}
|
||||
methodNames = getMethods().stream()
|
||||
.map(PythonTypeStubMethod::getName)
|
||||
.collect(Collectors.toCollection(() -> new HashSet<>(getMethods().size())));
|
||||
return methodNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a set of the public field names for this type
|
||||
*
|
||||
* @return the set of public field names
|
||||
*/
|
||||
private Set<String> getFieldNames() {
|
||||
if (fieldNames != null) {
|
||||
return fieldNames;
|
||||
}
|
||||
fieldNames = getFields().stream()
|
||||
.map(VariableElement::getSimpleName)
|
||||
.map(Object::toString)
|
||||
.map(PythonTypeStubElement::sanitize)
|
||||
.collect(Collectors.toCollection(() -> new HashSet<>(getFields().size())));
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an appropriate Python generic base for the provided type
|
||||
*
|
||||
* @param type the generic type
|
||||
* @param params the type parameters
|
||||
* @return the parameterized generic base type
|
||||
*/
|
||||
private static String getGenericBase(String type, Iterable<String> params) {
|
||||
String generic = GENERIC_CUSTOMIZERS.getOrDefault(type, "typing.Generic");
|
||||
return generic + "[" + String.join(", ", params) + "]";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the first part of the Python class definition
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the indentation
|
||||
*/
|
||||
private void printClassDefinition(PrintWriter printer, String indent) {
|
||||
if (!isPublic(el)) {
|
||||
printer.print(indent);
|
||||
printer.println("@typing.type_check_only");
|
||||
}
|
||||
if (doclet.isDeprecated(el)) {
|
||||
String msg = doclet.getDeprecatedMessage(el);
|
||||
if (msg != null) {
|
||||
// a message is required
|
||||
// if one is not present, don't apply it
|
||||
printer.print(indent);
|
||||
printer.print("@deprecated(");
|
||||
printer.print(msg);
|
||||
printer.println(')');
|
||||
}
|
||||
}
|
||||
printer.print(indent);
|
||||
printer.print("class ");
|
||||
printer.print(getName());
|
||||
|
||||
String base = getSuperClass();
|
||||
if (base == null) {
|
||||
// edge case, this is java.lang.Object
|
||||
printer.println(":");
|
||||
indent = indent(indent);
|
||||
writeJavaDoc(printer, indent);
|
||||
printer.println();
|
||||
return;
|
||||
}
|
||||
|
||||
Stream<String> bases;
|
||||
if (el.getInterfaces().isEmpty()) {
|
||||
bases = Stream.of(base);
|
||||
}
|
||||
else if (base.equals(OBJECT_NAME)) {
|
||||
// Object base isn't needed
|
||||
bases = getInterfaces();
|
||||
}
|
||||
else {
|
||||
bases = Stream.concat(Stream.of(base), getInterfaces());
|
||||
}
|
||||
|
||||
List<String> typeParams = getClassTypeVars();
|
||||
if (!typeParams.isEmpty()) {
|
||||
String type = el.getQualifiedName().toString();
|
||||
String genericBase = getGenericBase(type, typeParams);
|
||||
bases = Stream.concat(bases, Stream.of(genericBase));
|
||||
}
|
||||
|
||||
Iterator<String> it = bases.iterator();
|
||||
String baseList = String.join(", ", (Iterable<String>) () -> it);
|
||||
if (!baseList.isEmpty()) {
|
||||
printer.print("(");
|
||||
printer.print(baseList);
|
||||
printer.print(")");
|
||||
}
|
||||
printer.println(":");
|
||||
indent = indent(indent);
|
||||
if (getNestedTypes().isEmpty() && getFields().isEmpty() && getMethods().isEmpty()) {
|
||||
writeJavaDoc(printer, indent, "...");
|
||||
}
|
||||
else {
|
||||
writeJavaDoc(printer, indent);
|
||||
}
|
||||
printer.println();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided float constant to a Python constant
|
||||
*
|
||||
* @param value the value
|
||||
* @return the Python float constant
|
||||
*/
|
||||
private static String convertFloatConstant(double value) {
|
||||
if (Double.isInfinite(value)) {
|
||||
if (value < 0.0f) {
|
||||
return "float(\"-inf\")";
|
||||
}
|
||||
return "float(\"inf\")";
|
||||
}
|
||||
if (Double.isNaN(value)) {
|
||||
return "float(\"nan\")";
|
||||
}
|
||||
return Double.toString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the provided field to a Python constant if applicable
|
||||
*
|
||||
* @param field the field
|
||||
* @return the constant value or null
|
||||
*/
|
||||
private String getConstantValue(VariableElement field) {
|
||||
Object value = field.getConstantValue();
|
||||
return switch (value) {
|
||||
case String str -> doclet.getStringLiteral(str);
|
||||
case Character str -> doclet.getStringLiteral(str);
|
||||
case Boolean flag -> flag ? "True" : "False";
|
||||
case Float dec -> convertFloatConstant(dec);
|
||||
case Double dec -> convertFloatConstant(dec);
|
||||
case null -> null;
|
||||
default -> value.toString();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this element is an undocumented override
|
||||
*
|
||||
* @param child the element to check
|
||||
* @return true if this override has no additional documentation
|
||||
*/
|
||||
private boolean isUndocumentedOverride(Element child) {
|
||||
if (!doclet.hasJavadoc(child)) {
|
||||
return child.getAnnotation(Override.class) != null;
|
||||
}
|
||||
if (doclet.hasJavadocTag(child, DocTree.Kind.INHERIT_DOC)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this element is visible
|
||||
*
|
||||
* @param child the element to check
|
||||
* @param protectedScope true to include protected scope
|
||||
* @return true if this element is visible
|
||||
*/
|
||||
private boolean isVisible(Element child, boolean protectedScope) {
|
||||
if (isPublic(child)) {
|
||||
return true;
|
||||
}
|
||||
if (protectedScope) {
|
||||
return isProtected(child);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the base class to use for this type
|
||||
*
|
||||
* @return the base class
|
||||
*/
|
||||
private String getSuperClass() {
|
||||
TypeMirror base = el.getSuperclass();
|
||||
if (base.getKind() == TypeKind.NONE) {
|
||||
if (el.getQualifiedName().toString().equals(OBJECT_NAME)) {
|
||||
return null;
|
||||
}
|
||||
return OBJECT_NAME;
|
||||
}
|
||||
return sanitizeQualifiedName(el, base);
|
||||
}
|
||||
|
||||
private String sanitizeQualifiedName(TypeMirror type) {
|
||||
return sanitizeQualifiedName(el, type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the interfaces for this type
|
||||
*
|
||||
* @return the interfaces
|
||||
*/
|
||||
private Stream<String> getInterfaces() {
|
||||
return el.getInterfaces()
|
||||
.stream()
|
||||
.map(this::sanitizeQualifiedName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating a Python property
|
||||
*/
|
||||
class Property {
|
||||
PythonTypeStubMethod.PropertyMethod getter;
|
||||
PythonTypeStubMethod.PropertyMethod setter;
|
||||
|
||||
/**
|
||||
* Prints this property to the provided printer
|
||||
*
|
||||
* @param printer the printer
|
||||
* @param indent the indentation
|
||||
*/
|
||||
void process(PrintWriter printer, String indent) {
|
||||
if (getter == null) {
|
||||
// only possible at runtime
|
||||
return;
|
||||
}
|
||||
String name = getter.getName();
|
||||
if (name.equals("property")) {
|
||||
// it's not a keyword but it makes the type checker go haywire
|
||||
// just blacklist it
|
||||
return;
|
||||
}
|
||||
if (getMethodNames().contains(name) || getFieldNames().contains(name)) {
|
||||
// do not redefine a method or field
|
||||
return;
|
||||
}
|
||||
String type = sanitizeQualifiedName(getter.getType());
|
||||
printer.print(indent);
|
||||
printer.println("@property");
|
||||
printer.print(indent);
|
||||
printer.print("def ");
|
||||
printer.print(name);
|
||||
printer.print("(self) -> ");
|
||||
printer.print(type);
|
||||
printer.println(":");
|
||||
indent = indent(indent);
|
||||
printer.print(indent);
|
||||
printer.println("...");
|
||||
printer.println();
|
||||
|
||||
if (setter != null) {
|
||||
indent = deindent(indent);
|
||||
printer.print(indent);
|
||||
printer.print("@");
|
||||
printer.print(name);
|
||||
printer.println(".setter");
|
||||
printer.print(indent);
|
||||
printer.print("def ");
|
||||
printer.print(name);
|
||||
printer.print("(self, value: ");
|
||||
printer.print(type);
|
||||
printer.println("):");
|
||||
indent = indent(indent);
|
||||
printer.print(indent);
|
||||
printer.println("...");
|
||||
printer.println();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,356 @@
|
|||
package ghidra.doclets.typestubs;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.lang.model.element.Element;
|
||||
|
||||
import com.sun.source.doctree.DocTree;
|
||||
|
||||
/**
|
||||
* Helper class for converting an HTML table to reStructuredText
|
||||
*/
|
||||
final class RstTableBuilder {
|
||||
|
||||
// give each column enough padding to allow an alignment
|
||||
private static final int COLUMN_PADDING = 2;
|
||||
|
||||
private final HtmlConverter docConverter;
|
||||
private final Element el;
|
||||
private Row columns = new Row();
|
||||
private List<Row> rows = new ArrayList<>();
|
||||
private Row currentRow = null;
|
||||
private List<Integer> columnWidths = new ArrayList<>();
|
||||
private String caption = null;
|
||||
|
||||
/**
|
||||
* Creates a new {@link RstTableBuilder}
|
||||
*
|
||||
* @param docConverter the html converter
|
||||
* @param el the element
|
||||
*/
|
||||
RstTableBuilder(HtmlConverter docConverter, Element el) {
|
||||
this.docConverter = docConverter;
|
||||
this.el = el;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new row group to the table
|
||||
*
|
||||
* @param tree the html tree containing the row group
|
||||
* @throws UnsupportedOperationException if any row in the group contains a nested row
|
||||
*/
|
||||
void addRowGroup(HtmlDocTree tree) {
|
||||
switch (tree.getHtmlKind()) {
|
||||
case THEAD:
|
||||
if (tree.getBody().size() > 1) {
|
||||
throw new UnsupportedOperationException("nested table rows are not supported");
|
||||
}
|
||||
case TBODY:
|
||||
case TFOOT:
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (!(tag instanceof HtmlDocTree)) {
|
||||
continue;
|
||||
}
|
||||
addRow((HtmlDocTree) tag);
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new row to the table
|
||||
*
|
||||
* @param tree the html tree containing the row
|
||||
* @throws UnsupportedOperationException if the row contains a nested row
|
||||
*/
|
||||
void addRow(HtmlDocTree tree) {
|
||||
if (currentRow == null) {
|
||||
currentRow = columns;
|
||||
}
|
||||
else {
|
||||
currentRow = new Row();
|
||||
rows.add(currentRow);
|
||||
}
|
||||
boolean columnsDone = columns.size() > 0;
|
||||
for (DocTree tag : tree.getBody()) {
|
||||
if (!(tag instanceof HtmlDocTree)) {
|
||||
continue;
|
||||
}
|
||||
HtmlDocTree html = (HtmlDocTree) tag;
|
||||
String align = docConverter.getAttributes(el, html.getStartTag()).get("align");
|
||||
switch (html.getHtmlKind()) {
|
||||
case TH:
|
||||
if (columnsDone) {
|
||||
// vertical headers
|
||||
// insert it as an entry so it at least comes out ok
|
||||
addEntry(getBody(html), align);
|
||||
}
|
||||
else {
|
||||
addColumn(getBody(html), align);
|
||||
}
|
||||
break;
|
||||
case TD:
|
||||
addEntry(getBody(html), align);
|
||||
break;
|
||||
case TR:
|
||||
throw new UnsupportedOperationException("nested table rows are not supported");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a caption to the table
|
||||
*
|
||||
* @param caption the caption
|
||||
*/
|
||||
void addCaption(String caption) {
|
||||
if (!caption.isBlank()) {
|
||||
this.caption = caption;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the reStructuredText formatted table
|
||||
*
|
||||
* @return the reStructuredText table
|
||||
*/
|
||||
String build() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append('\n');
|
||||
|
||||
if (caption != null) {
|
||||
int length = caption.length();
|
||||
builder.repeat('^', length)
|
||||
.append('\n');
|
||||
builder.append(caption)
|
||||
.append('\n')
|
||||
.repeat('^', length)
|
||||
.append('\n');
|
||||
}
|
||||
|
||||
buildRowBorder(builder, '-');
|
||||
columns.build(builder);
|
||||
buildRowBorder(builder, '=');
|
||||
|
||||
for (Row row : rows) {
|
||||
row.build(builder);
|
||||
buildRowBorder(builder, '-');
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a column to the table
|
||||
*
|
||||
* @param value the column value
|
||||
* @param align the column alignment
|
||||
*/
|
||||
private void addColumn(String value, String align) {
|
||||
if (align == null) {
|
||||
align = "CENTER";
|
||||
}
|
||||
addColumn(value, Alignment.valueOf(align.toUpperCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a column to the table
|
||||
*
|
||||
* @param value the column value
|
||||
* @param align the column alignment
|
||||
*/
|
||||
private void addColumn(String value, Alignment align) {
|
||||
int column = columns.size();
|
||||
columns.addValue(value, align);
|
||||
growColumn(value, column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry to the current row in the table
|
||||
*
|
||||
* @param value the entry value
|
||||
* @param align the entry alignment
|
||||
*/
|
||||
private void addEntry(String value, String align) {
|
||||
if (align == null) {
|
||||
align = "LEFT";
|
||||
}
|
||||
addEntry(value, Alignment.valueOf(align.toUpperCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entry to the current row in the table
|
||||
*
|
||||
* @param value the entry value
|
||||
* @param align the entry alignment
|
||||
*/
|
||||
private void addEntry(String value, Alignment align) {
|
||||
int column = currentRow.size();
|
||||
currentRow.addValue(value, align);
|
||||
growColumn(value, column);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to get the converted contents of an html tree
|
||||
*
|
||||
* @param tag the html
|
||||
* @return the converted html
|
||||
*/
|
||||
private String getBody(HtmlDocTree tag) {
|
||||
return docConverter.convertTree(el, tag.getBody());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a row border with the provided character
|
||||
*
|
||||
* @param builder the string builder
|
||||
* @param c the border character
|
||||
*/
|
||||
private void buildRowBorder(StringBuilder builder, char c) {
|
||||
builder.append('+');
|
||||
for (int width : columnWidths) {
|
||||
builder.repeat(c, width)
|
||||
.append('+');
|
||||
}
|
||||
builder.append('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the max line width for the provided multi-line text
|
||||
*
|
||||
* @param text the text
|
||||
* @return the max line width
|
||||
*/
|
||||
private static int getLineWidth(String text) {
|
||||
// value may be mutiple lines
|
||||
return text.lines()
|
||||
.map(String::stripLeading)
|
||||
.mapToInt(String::length)
|
||||
.max()
|
||||
.getAsInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grows the provided column appropriately for the newly added value
|
||||
*
|
||||
* @param value the newly added value
|
||||
* @param column the column number
|
||||
*/
|
||||
private void growColumn(String value, int column) {
|
||||
int length = !value.isEmpty() ? getLineWidth(value) + COLUMN_PADDING : COLUMN_PADDING;
|
||||
if (column >= columnWidths.size()) {
|
||||
columnWidths.add(length);
|
||||
return;
|
||||
}
|
||||
if (columnWidths.get(column) < length) {
|
||||
columnWidths.set(column, length);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the single line value according to the column width and alignment
|
||||
*
|
||||
* @param value the value to align
|
||||
* @param columnWidth the column width
|
||||
* @param align the alignment
|
||||
* @return the aligned value
|
||||
*/
|
||||
private static String alignSingleLine(String value, int columnWidth, Alignment align) {
|
||||
int length = value.length();
|
||||
return switch (align) {
|
||||
case LEFT -> value + " ".repeat(columnWidth - length);
|
||||
case CENTER -> {
|
||||
int left = (columnWidth - length) / 2;
|
||||
int right = left;
|
||||
if (left + right + length < columnWidth) {
|
||||
right++;
|
||||
}
|
||||
yield " ".repeat(left) + value + " ".repeat(right);
|
||||
}
|
||||
case RIGHT -> " ".repeat(columnWidth - length) + value;
|
||||
};
|
||||
}
|
||||
|
||||
private static enum Alignment {
|
||||
LEFT,
|
||||
CENTER,
|
||||
RIGHT
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for modeling a table row
|
||||
*/
|
||||
private class Row {
|
||||
int maxLines = 1;
|
||||
List<List<String>> values = new ArrayList<>();
|
||||
List<Alignment> alignments = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Adds the value to the row
|
||||
*
|
||||
* @param value the value
|
||||
* @param align the alignment
|
||||
*/
|
||||
void addValue(String value, Alignment align) {
|
||||
List<String> lines = value.lines()
|
||||
.map(String::stripLeading)
|
||||
.collect(Collectors.toList());
|
||||
if (lines.size() > maxLines) {
|
||||
maxLines = lines.size();
|
||||
}
|
||||
values.add(lines);
|
||||
alignments.add(align);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the size of this row
|
||||
*
|
||||
* @return the row size
|
||||
*/
|
||||
int size() {
|
||||
return values.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends this row to the provided string builder
|
||||
*
|
||||
* @param builder the string builder
|
||||
*/
|
||||
void build(StringBuilder builder) {
|
||||
for (int i = 0; i < maxLines; i++) {
|
||||
builder.append('|');
|
||||
for (int j = 0; j < values.size(); j++) {
|
||||
List<String> entry = values.get(j);
|
||||
String value;
|
||||
if (i >= entry.size()) {
|
||||
value = " ".repeat(columnWidths.get(j));
|
||||
}
|
||||
else {
|
||||
value = alignSingleLine(j, entry.get(i));
|
||||
}
|
||||
builder.append(value)
|
||||
.append('|');
|
||||
}
|
||||
builder.append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aligns the provided single line value according to the column and its alignent
|
||||
*
|
||||
* @param column the column number
|
||||
* @param value the single line value
|
||||
* @return the aligned value
|
||||
*/
|
||||
String alignSingleLine(int column, String value) {
|
||||
int columnLength = columnWidths.get(column);
|
||||
return RstTableBuilder.alignSingleLine(value, columnLength, alignments.get(column));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue