diff --git a/.gitignore b/.gitignore index 01600b3..c2f3d0a 100644 --- a/.gitignore +++ b/.gitignore @@ -115,4 +115,5 @@ gradle-app.setting # End of https://www.toptal.com/developers/gitignore/api/eclipse,java,gradle .classpath -*.d.ts \ No newline at end of file +*.d.ts +.idea diff --git a/README.md b/README.md index c631171..954c225 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,22 @@ This is a command-line application. * By default, everything is included * --exclude: prefixes for excluded paths * Processed after includes; nothing is excluded by default -* --blacklist: blacklisted type fragments - * Types that have names which contain any of these are omitted +* --blacklist: blacklisted type regular expression patterns + * Types that have names which equals any of these are omitted * Methods and fields that would use them are also omitted! * --packageJson: read these options from a JSON file * The options should be placed under `tsbindOptions` object * Names of options lack -- prefixes but are otherwise same * Handy when you already have package.json for publishing * --index: generate index.d.ts that references other generated files +* --emitReadOnly : if set, deactivates constructors and setter in the generated types +* --excludeMethods : a list of regular expressions that will be used to exclude methods by name +* --groupByModule : if set, the generated types will be grouped by module name, if not set the output will instead be grouped by domain +* --gettersAndSettersOff : if false, Typescript getters and setters will be used to group methods into setters and getters. If true this mechanism is disabled and no intelligence will be performed to regroup getter and setter methods. +* --methodWhitelist : a list of methods using regular expressions that if they match they will be the only methods retained in the generated types +* --flattenTypes : if set the generated types will be flattened, which might that all the inherited methods will be included in the generated types and inheritance will be removed. This makes it possible to reduce the number of types for APIs +* --forceParentJavadocs : if set it will always copy javadocs if they exist on parent types and don't exist locally. +* --debugMatching: if set it will output some useful debug information about the black/white listing mechanism ## Limitations java-ts-bind does not necessarily generate *valid* TypeScript declarations. @@ -44,4 +52,4 @@ Please also note that java-ts-bind provides *only the types*. Implementing a module loading system for importing them is left as an exercise for the reader. For pointers, see [CraftJS](https://github.com/Valtakausi/craftjs) which (at time of writing) implements a CommonJS module loader with -Java and TypeScript. \ No newline at end of file +Java and TypeScript. diff --git a/build.gradle b/build.gradle index 2de75cf..64398b6 100644 --- a/build.gradle +++ b/build.gradle @@ -8,13 +8,12 @@ repositories { } compileJava { - options.release = 16 } dependencies { implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.22.1' implementation 'com.google.code.gson:gson:2.8.6' - implementation 'com.beust:jcommander:1.81' + implementation 'org.jcommander:jcommander:1.83' implementation 'org.jsoup:jsoup:1.13.1' } diff --git a/src/main/java/io/github/bensku/tsbind/AstGenerator.java b/src/main/java/io/github/bensku/tsbind/AstGenerator.java index db79463..6c86b0b 100644 --- a/src/main/java/io/github/bensku/tsbind/AstGenerator.java +++ b/src/main/java/io/github/bensku/tsbind/AstGenerator.java @@ -8,6 +8,7 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import java.util.regex.Pattern; import java.util.stream.Collectors; import com.github.javaparser.JavaParser; @@ -54,18 +55,29 @@ public class AstGenerator { private final JavaParser parser; - + /** * Blacklisted type name fragments. Types that match any of these are never * emitted. All {@link Member members} that contain them are also ignored. */ - private final List blacklist; - - public AstGenerator(JavaParser parser, List blacklist) { + private final List blacklistPatterns; + + private final List methodWhiteListPatterns; + + private final List fieldWhiteListPatterns; + private boolean gettersAndSettersOff; + + private boolean debugMatching; + + public AstGenerator(JavaParser parser, List blacklist, List methodWhiteList, List fieldWhiteList, boolean gettersAndSettersOff, boolean debugMatching) { this.parser = parser; - this.blacklist = blacklist; + this.blacklistPatterns = blacklist.stream().map(Pattern::compile).collect(Collectors.toList()); + this.gettersAndSettersOff = gettersAndSettersOff; + this.methodWhiteListPatterns = methodWhiteList.stream().map(Pattern::compile).collect(Collectors.toList()); + this.fieldWhiteListPatterns = fieldWhiteList.stream().map(Pattern::compile).collect(Collectors.toList()); + this.debugMatching = debugMatching; } - + /** * Parses type AST from source code. * @param source Source unit (single Java file). @@ -73,7 +85,7 @@ public AstGenerator(JavaParser parser, List blacklist) { */ public Optional parseType(SourceUnit source) { // FIXME don't log errors here, CLI might not be only user in future - + ParseResult result = parser.parse(source.code); if (!result.isSuccessful()) { //throw new IllegalArgumentException("failed to parse given source code: " + result.getProblems()); @@ -99,7 +111,7 @@ public Optional parseType(SourceUnit source) { return Optional.empty(); } } - + private List getParameters(ResolvedMethodLikeDeclaration method, Boolean[] nullable) { List params = new ArrayList<>(method.getNumberOfParams()); for (int i = 0; i < method.getNumberOfParams(); i++) { @@ -109,7 +121,7 @@ private List getParameters(ResolvedMethodLikeDeclaration method, Bool } return params; } - + private String getJavadoc(Node node) { return node.getComment().map(comment -> { if (comment.isJavadocComment()) { @@ -118,36 +130,83 @@ private String getJavadoc(Node node) { return null; }).orElse(null); } - + /** * Checks if a member is or uses blacklisted types. - * @param member Member to check. + * @param node Member to check. * @return Whether the member should be omitted. */ - private boolean isBlacklisted(AstNode node) { + private boolean isBlacklisted(AstNode node, String typeName, String prefix) { // If this is a type reference or declaration, check if it is blacklisted if (node instanceof TypeRef || node instanceof TypeDefinition) { TypeRef ref = node instanceof TypeDefinition ? ((TypeDefinition) node).ref : (TypeRef) node; String name = ref.name(); - for (String fragment : blacklist) { - if (name.contains(fragment)) { + for (Pattern blacklistPattern : blacklistPatterns) { + if (blacklistPattern.matcher(name).matches()) { + if (debugMatching) { + System.out.println(prefix + " Blacklisted: " + node + " (from blacklist match)"); + } return true; // Blacklisted } } return false; // Not blacklisted! } - + + boolean whitelisted = false; + if (!methodWhiteListPatterns.isEmpty()) { + if (node instanceof Method) { + Method method = (Method) node; + for (Pattern pattern : methodWhiteListPatterns) { + if (pattern.matcher(typeName + "." + method.name()).matches()) { + if (debugMatching) { + System.out.println(prefix + " Whitelisted: " + node + " (from method whitelist match)"); + } + whitelisted = true; + break; + } + } + } + } + if (!fieldWhiteListPatterns.isEmpty()) { + if (node instanceof Field) { + Field field = (Field) node; + for (Pattern pattern : fieldWhiteListPatterns) { + if (pattern.matcher(typeName + "." + field.name).matches()) { + if (debugMatching) { + System.out.println(prefix + " Whitelisted: " + node + " (from field whitelist match)"); + } + whitelisted = true; + break; + } + } + } + } + if (node instanceof Parameter) { + whitelisted = true; + } + // Check non-type node children AtomicBoolean childBlacklisted = new AtomicBoolean(false); node.walk(n -> { // Avoid infinite recursion by excluding node given to us as parameter - if (n != node && isBlacklisted(n)) { + if (n != node && isBlacklisted(n, typeName, prefix + " ")) { childBlacklisted.setPlain(true); // Blacklisted + if (debugMatching) { + System.out.println(prefix + " Blacklisted by child node: " + n); + } } }); - return childBlacklisted.getPlain(); + if (childBlacklisted.getPlain()) { + return true; + } + if (!whitelisted) { + if (debugMatching) { + System.out.println(prefix + " Blacklisted: " + typeName + " for node " + node); + } + } + return !whitelisted; } - + private void processMember(String typeName, TypeDeclaration type, TypeDefinition.Kind typeKind, Set privateOverrides, boolean lombokGetter, boolean lombokSetter, BodyDeclaration member, Consumer addMember) { @@ -155,20 +214,20 @@ private void processMember(String typeName, TypeDeclaration type, TypeDefinit if (member.isFieldDeclaration()) { // Even private fields may need Lombok getter/setter try { - processField(addMember, member.asFieldDeclaration(), typeKind == TypeDefinition.Kind.INTERFACE, isPublic, lombokGetter, lombokSetter); + processField(addMember, member.asFieldDeclaration(), typeKind == TypeDefinition.Kind.INTERFACE, isPublic, lombokGetter, lombokSetter, typeName); } catch (UnsolvedSymbolException e) { // Allow symbol lookup to fail on private fields if (isPublic) { throw e; } } - } + } if (!isPublic) { // For now, only private fields are needed // Work as if other non-public members did not exist return; // Neither implicitly or explicitly public } - + // Process type depending on what it is if (member.isTypeDeclaration()) { // Recursively process an inner type @@ -181,37 +240,37 @@ private void processMember(String typeName, TypeDeclaration type, TypeDefinit // Constructor might be generic, but AFAIK TypeScript doesn't support that // (constructors of generic classes are, of course, supported) // Private constructors are not yet needed, so they won't exist - addMember.accept(new Constructor(constructor.getName(), getParameters(constructor, nullable), getJavadoc(member), true)); + addMember.accept(new Constructor(constructor.getName(), getParameters(constructor, nullable), getJavadoc(member), true, typeName)); } else if (member.isMethodDeclaration()) { - addMember.accept(processMethod(member.asMethodDeclaration(), privateOverrides)); + addMember.accept(processMethod(member.asMethodDeclaration(), privateOverrides, typeName)); } } - + private Optional processType(String typeName, TypeDeclaration type) { - ResolvedReferenceTypeDeclaration resolved = type.resolve(); + ResolvedReferenceTypeDeclaration resolved = type.resolve(); TypeRef typeRef = TypeRef.fromDeclaration(typeName, resolved); List members = new ArrayList<>(); - + // Create a lambda to support filtering members before they're added Consumer addMember = (member) -> { - if (!isBlacklisted(member)) { + if (!isBlacklisted(member, typeName, "")) { members.add(member); } }; - + // If this is an enum, generate enum constants and compiler-generated methods // JavaParser doesn't consider enum constants "members" if (type.isEnumDeclaration()) { for (EnumConstantDeclaration constant : type.asEnumDeclaration().getEntries()) { addMember.accept(new Field(constant.getNameAsString(), typeRef, getJavadoc(constant), true, true, true)); } - + addMember.accept(new Method("valueOf", typeRef, List.of(new Parameter("name", TypeRef.STRING, false)), - List.of(), null, true, true, false)); - addMember.accept(new Method("values", typeRef.makeArray(1), List.of(), List.of(), null, true, true, false)); + List.of(), null, true, true, false, typeName)); + addMember.accept(new Method("values", typeRef.makeArray(1), List.of(), List.of(), null, true, true, false, typeName)); } - + // Figure out supertypes and interfaces (needed by some members) TypeDefinition.Kind typeKind; List superTypes; @@ -229,18 +288,18 @@ private Optional processType(String typeName, TypeDeclaration typeKind = TypeDefinition.Kind.FUNCTIONAL_INTERFACE; } } - + PublicFilterResult extendedResult = filterPublicTypes(decl.getExtendedTypes()); PublicFilterResult implementedResult = filterPublicTypes(decl.getImplementedTypes()); superTypes = extendedResult.publicTypes.stream() .map(TypeRef::fromType) - .filter(t -> !isBlacklisted(t)) - .toList(); + .filter(t -> !isBlacklisted(t, typeName, "")) + .collect(Collectors.toList()); interfaces = implementedResult.publicTypes.stream() .map(TypeRef::fromType) - .filter(t -> !isBlacklisted(t)) - .toList(); - + .filter(t -> !isBlacklisted(t, typeName, "")) + .collect(Collectors.toList()); + extendedResult.privateTypes.forEach(t -> privateOverrides.addAll(getAllMethods(t))); implementedResult.privateTypes.forEach(t -> privateOverrides.addAll(getAllMethods(t))); } else if (type.isEnumDeclaration()) { @@ -252,11 +311,11 @@ private Optional processType(String typeName, TypeDeclaration superTypes = List.of(); interfaces = List.of(); } - + // Lombok setter/getter support boolean lombokGetter = type.isAnnotationPresent("Getter"); boolean lombokSetter = type.isAnnotationPresent("Setter"); - + // Handle normal members for (BodyDeclaration member : type.getMembers()) { try { @@ -266,7 +325,7 @@ private Optional processType(String typeName, TypeDeclaration System.out.println("unresolved symbol " + e.getName() + " in " + typeName + "; omitting member"); } } - + // Lombok autogenerated constructors // AllArgsConstructor uses ALL instance fields // RequiredArgsConstructor only for final and (TODO, not implemented) non-null fields @@ -276,7 +335,7 @@ private Optional processType(String typeName, TypeDeclaration .map(member -> (Field) member) .map(field -> new Parameter(field.name, field.type, false)) .collect(Collectors.toList()); - addMember.accept(new Constructor(type.getNameAsString(), params, null, true)); + addMember.accept(new Constructor(type.getNameAsString(), params, null, true, typeName)); } if (type.isAnnotationPresent("RequiredArgsConstructor")) { List params = members.stream() @@ -285,9 +344,9 @@ private Optional processType(String typeName, TypeDeclaration .filter(field -> field.isFinal) .map(field -> new Parameter(field.name, field.type, false)) .collect(Collectors.toList()); - addMember.accept(new Constructor(type.getNameAsString(), params, null, true)); + addMember.accept(new Constructor(type.getNameAsString(), params, null, true, typeName)); } - + // Create type definition String javadoc = getJavadoc(type); return Optional.of(new TypeDefinition(javadoc, type.isStatic(), typeRef, typeKind, isAbstract, @@ -298,7 +357,7 @@ private static class PublicFilterResult { public final List publicTypes = new ArrayList<>(); public final List privateTypes = new ArrayList<>(); } - + private PublicFilterResult filterPublicTypes(List types) { PublicFilterResult result = new PublicFilterResult(); for (ClassOrInterfaceType type : types) { @@ -311,7 +370,7 @@ private PublicFilterResult filterPublicTypes(List types) { } return result; } - + private Set getAllMethods(ResolvedReferenceType type) { if (type == null) { return Collections.emptySet(); @@ -333,7 +392,7 @@ private Set getAllMethods(ResolvedReferenceType type) { } return names; } - + private boolean isPublic(TypeDeclaration type, BodyDeclaration member) { // JPMS is ignored for now, would need to parse module infos for that AccessSpecifier access = (member instanceof NodeWithModifiers) @@ -347,10 +406,10 @@ private boolean isPublic(TypeDeclaration type, BodyDeclaration member) { return type.asClassOrInterfaceDeclaration().isInterface(); } // Enum constants are handled separately, JavaParser doesn't consider them members - + return false; // No reason to consider member public } - + private boolean isPublic(ResolvedReferenceTypeDeclaration type) { // Special case for functional interfaces that are converted to function types // (you obviously can't extend those in TypeScript) @@ -362,59 +421,59 @@ private boolean isPublic(ResolvedReferenceTypeDeclaration type) { } return false; } - - private Method processMethod(MethodDeclaration member, Set privateOverrides) { + + private Method processMethod(MethodDeclaration member, Set privateOverrides, String typeName) { boolean isPublic = true; // Private methods are not yet needed, so they won't exist - + ResolvedMethodDeclaration method = member.asMethodDeclaration().resolve(); boolean nullableReturn = member.isAnnotationPresent("Nullable"); Boolean[] nullableParams = member.getParameters().stream() .map(param -> param.isAnnotationPresent("Nullable")).toArray(Boolean[]::new); - + String name = method.getName(); TypeRef returnType = TypeRef.fromType(method.getReturnType(), nullableReturn); String methodDoc = getJavadoc(member); boolean override = !privateOverrides.contains(name) && member.getAnnotationByClass(Override.class).isPresent(); // boolean getters and setters are kept as regular methods to prevent confusing naming - if (name.length() > 3 && name.startsWith("get") && returnType != TypeRef.VOID + if (!gettersAndSettersOff && name.length() > 3 && name.startsWith("get") && returnType != TypeRef.VOID && returnType != TypeRef.BOOLEAN && method.getNumberOfParams() == 0 && method.getTypeParameters().isEmpty()) { // GraalJS will make this getter work, somehow - return new Getter(name, returnType, methodDoc, isPublic, method.isStatic(), override); - } else if (name.length() > 3 && name.startsWith("set") && method.getNumberOfParams() == 1 + return new Getter(name, returnType, methodDoc, isPublic, method.isStatic(), override, typeName); + } else if (!gettersAndSettersOff && name.length() > 3 && name.startsWith("set") && method.getNumberOfParams() == 1 && TypeRef.fromType(method.getParam(0).getType()) != TypeRef.BOOLEAN && method.getTypeParameters().isEmpty()) { // GraalJS will make this setter work, somehow return new Setter(name, TypeRef.fromType(method.getParam(0).getType(), - nullableParams[0]), methodDoc, isPublic, method.isStatic(), override); + nullableParams[0]), methodDoc, isPublic, method.isStatic(), override, typeName); } else { // Normal method // Resolve type parameters and add to member list return new Method(name, returnType, getParameters(method, nullableParams), method.getTypeParameters().stream().map(TypeRef::fromDeclaration).collect(Collectors.toList()), - methodDoc, isPublic, method.isStatic(), override); + methodDoc, isPublic, method.isStatic(), override, typeName); } } - + private void processField(Consumer addMember, FieldDeclaration member, boolean isInterface, - boolean isPublic, boolean lombokGetter, boolean lombokSetter) { + boolean isPublic, boolean lombokGetter, boolean lombokSetter, String typeName) { FieldDeclaration field = member.asFieldDeclaration(); boolean nullable = field.isAnnotationPresent("Nullable"); NodeList vars = field.getVariables(); - boolean isStatic = isInterface || field.isStatic(); - boolean isFinal = isInterface || field.isFinal(); + boolean isStatic = !isInterface && field.isStatic(); + boolean isFinal = !isInterface && field.isFinal(); if (vars.size() == 1) { FieldProps props = new FieldProps(field.resolve(), getJavadoc(member), nullable, isPublic, isStatic, isFinal, lombokGetter, lombokSetter); - processFieldValue(addMember, props); + processFieldValue(addMember, props, typeName); } else { // Symbol solver can't resolve this for us for (VariableDeclarator var : vars) { FieldProps props = new FieldProps(var.resolve(), getJavadoc(member), nullable, isPublic, isStatic, isFinal, lombokGetter, lombokSetter); - processFieldValue(addMember, props); + processFieldValue(addMember, props, typeName); } } } - + private class FieldProps { ResolvedValueDeclaration value; String javadoc; @@ -423,7 +482,7 @@ private class FieldProps { boolean isStatic; boolean isFinal; boolean lombokGetter, lombokSetter; - + FieldProps(ResolvedValueDeclaration value, String javadoc, boolean nullable, boolean isPublic, boolean isStatic, boolean isFinal, boolean lombokGetter, boolean lombokSetter) { this.value = value; @@ -436,19 +495,20 @@ private class FieldProps { this.lombokSetter = lombokSetter; } } - - private void processFieldValue(Consumer addMember, FieldProps props) { + + private void processFieldValue(Consumer addMember, FieldProps props, String typeName) { TypeRef type = TypeRef.fromType(props.value.getType(), props.nullable); // Add normal field to AST addMember.accept(new Field(props.value.getName(), type, props.javadoc, props.isPublic, props.isStatic, props.isFinal)); - + // Generate public getter/setter pair for field (Lombok) if (props.lombokGetter) { - addMember.accept(new Getter(props.value.getName(), type, props.javadoc, true, props.isStatic, false)); + addMember.accept(new Getter(props.value.getName(), type, props.javadoc, true, props.isStatic, false, typeName)); } if (props.lombokSetter) { - addMember.accept(new Setter(props.value.getName(), type, props.javadoc, true, props.isStatic, false)); + addMember.accept(new Setter(props.value.getName(), type, props.javadoc, true, props.isStatic, false, typeName)); } } + } diff --git a/src/main/java/io/github/bensku/tsbind/ast/AstNode.java b/src/main/java/io/github/bensku/tsbind/ast/AstNode.java index 557ab6e..17d1ccc 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/AstNode.java +++ b/src/main/java/io/github/bensku/tsbind/ast/AstNode.java @@ -9,4 +9,10 @@ public interface AstNode { * @param visitor Visitor to be called for node found.. */ void walk(Consumer visitor); + + /** + * Require the toString method for all node implementation to make it easier to generate debugging output + * @return a String representation of the node's fields. + */ + String toString(); } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Constructor.java b/src/main/java/io/github/bensku/tsbind/ast/Constructor.java index f03c08a..d7216ed 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Constructor.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Constructor.java @@ -5,9 +5,13 @@ public class Constructor extends Method { - public Constructor(String name, List params, String javadoc, boolean isPublic) { - super(name, TypeRef.VOID, params, Collections.emptyList(), javadoc, isPublic, false, false); + public Constructor(String name, List params, String javadoc, boolean isPublic, String typeName) { + super(name, TypeRef.VOID, params, Collections.emptyList(), javadoc, isPublic, false, false, typeName); + } + + @Override + public String toString() { + return "new " + name; } - } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Field.java b/src/main/java/io/github/bensku/tsbind/ast/Field.java index 70dab47..f3d8a0c 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Field.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Field.java @@ -4,7 +4,7 @@ import java.util.function.Consumer; public class Field extends Member { - + /** * Field name. */ @@ -14,12 +14,12 @@ public class Field extends Member { * Type of the field. */ public final TypeRef type; - + /** * If this field is final (readonly). */ public final boolean isFinal; - + public Field(String name, TypeRef type, String javadoc, boolean isPublic, boolean isStatic, boolean isFinal) { super(javadoc, isPublic, isStatic); Objects.requireNonNull(name); @@ -39,4 +39,9 @@ public void walk(Consumer visitor) { public String name() { return name; } + + @Override + public String toString() { + return name + ": " + type; + } } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Getter.java b/src/main/java/io/github/bensku/tsbind/ast/Getter.java index 003e855..e310d89 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Getter.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Getter.java @@ -3,7 +3,7 @@ import java.util.Collections; public class Getter extends Method { - + public static String getterName(String methodName) { if (methodName.startsWith("is")) { return methodName.substring(2, 3).toLowerCase() + methodName.substring(3); @@ -13,16 +13,21 @@ public static String getterName(String methodName) { return methodName; } } - + private final String originalName; - public Getter(String name, TypeRef type, String javadoc, boolean isPublic, boolean isStatic, boolean isOverride) { - super(getterName(name), type, Collections.emptyList(), Collections.emptyList(), javadoc, isPublic, isStatic, isOverride); + public Getter(String name, TypeRef type, String javadoc, boolean isPublic, boolean isStatic, boolean isOverride, String typeName) { + super(getterName(name), type, Collections.emptyList(), Collections.emptyList(), javadoc, isPublic, isStatic, isOverride, typeName); this.originalName = name; } - + public String originalName() { return originalName; } + @Override + public String toString() { + return "get " + name + ": " + returnType; + } + } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Member.java b/src/main/java/io/github/bensku/tsbind/ast/Member.java index 1f43831..dabd5aa 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Member.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Member.java @@ -8,24 +8,29 @@ public abstract class Member implements AstNode { * Javadoc of this member, if it exists. */ public Optional javadoc; - + /** * Whether or not this member is public. Note that in some cases e.g. * interface members are implicitly public. */ public final boolean isPublic; - + /** * Whether or not this member is static. */ public final boolean isStatic; - + public Member(String javadoc, boolean isPublic, boolean isStatic) { this.javadoc = Optional.ofNullable(javadoc); this.isPublic = isPublic; this.isStatic = isStatic; } - + public abstract String name(); - + + @Override + public String toString() { + return "Member: " + name(); + } + } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Method.java b/src/main/java/io/github/bensku/tsbind/ast/Method.java index 7e8c84b..ff93b22 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Method.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Method.java @@ -14,32 +14,35 @@ public class Method extends Member { * Return type of the method. */ public final TypeRef returnType; - + /** * Parameters of the method */ public final List params; - + /** * Type (generic) parameters. */ public final List typeParams; - + /** * If this is annotated with {@link Override}. */ public final boolean isOverride; - + + public final String typeName; + public Method(String name, TypeRef returnType, List params, List typeParams, String javadoc, - boolean isPublic, boolean isStatic, boolean isOverride) { + boolean isPublic, boolean isStatic, boolean isOverride, String typeName) { super(javadoc, isPublic, isStatic); this.name = name; this.returnType = returnType; this.params = params; this.typeParams = typeParams; this.isOverride = isOverride; + this.typeName = typeName; } - + /** * Original name of the method. For setters and getters, this is different * from the {@link #name()}. @@ -62,4 +65,13 @@ public String name() { return name; } + public String typeName() { + return typeName; + } + + @Override + public String toString() { + return name + "(" + params + "): " + returnType; + } + } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Parameter.java b/src/main/java/io/github/bensku/tsbind/ast/Parameter.java index 4573cf4..2dc3232 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Parameter.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Parameter.java @@ -3,7 +3,7 @@ import java.util.function.Consumer; public class Parameter implements AstNode { - + /** * Name of the parameter. */ @@ -13,7 +13,7 @@ public class Parameter implements AstNode { * Parameter type. */ public final TypeRef type; - + /** * If this is a varargs parameter. */ @@ -31,4 +31,9 @@ public void walk(Consumer visitor) { type.walk(visitor); } + @Override + public String toString() { + return name + ": " + type; + } + } diff --git a/src/main/java/io/github/bensku/tsbind/ast/Setter.java b/src/main/java/io/github/bensku/tsbind/ast/Setter.java index 1118836..0eeec92 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/Setter.java +++ b/src/main/java/io/github/bensku/tsbind/ast/Setter.java @@ -12,16 +12,21 @@ public static String setterName(String methodName) { return methodName; } } - + private final String originalName; - public Setter(String name, TypeRef type, String javadoc, boolean isPublic, boolean isStatic, boolean isOverride) { + public Setter(String name, TypeRef type, String javadoc, boolean isPublic, boolean isStatic, boolean isOverride, String typeName) { super(setterName(name), TypeRef.VOID, List.of(new Parameter(setterName(name), type, false)), - Collections.emptyList(), javadoc, isPublic, isStatic, isOverride); + Collections.emptyList(), javadoc, isPublic, isStatic, isOverride, typeName); this.originalName = name; } - + public String originalName() { return originalName; } + + @Override + public String toString() { + return "set " + name + ": " + returnType; + } } diff --git a/src/main/java/io/github/bensku/tsbind/ast/TypeDefinition.java b/src/main/java/io/github/bensku/tsbind/ast/TypeDefinition.java index c08a5a0..e7afcda 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/TypeDefinition.java +++ b/src/main/java/io/github/bensku/tsbind/ast/TypeDefinition.java @@ -6,12 +6,12 @@ import java.util.function.Consumer; public class TypeDefinition extends Member { - + /** * Type reference to the defined type. */ public final TypeRef ref; - + public enum Kind { CLASS, INTERFACE, @@ -19,32 +19,32 @@ public enum Kind { ANNOTATION, FUNCTIONAL_INTERFACE } - + /** * What kind of type this is. */ public final Kind kind; - + /** * If this type is abstract. */ public final boolean isAbstract; - + /** * Superclasses (and interfaces, if this is an interface) of this type. */ public final List superTypes; - + /** * Interfaces of this type (if any). */ public final List interfaces; - + /** * List of members (methods, fields, inner types) of this type. */ public final List members; - + /** * Members that this type has. */ @@ -62,7 +62,7 @@ public TypeDefinition(String javadoc, boolean isStatic, TypeRef ref, Kind kind, this.memberNames = new HashSet<>(); members.stream().map(Member::name).forEach(memberNames::add); } - + public boolean hasMember(String name) { return memberNames.contains(name); } @@ -86,4 +86,8 @@ public String name() { return ref.name(); } + @Override + public String toString() { + return name(); + } } diff --git a/src/main/java/io/github/bensku/tsbind/ast/TypeRef.java b/src/main/java/io/github/bensku/tsbind/ast/TypeRef.java index d306867..a2b09c4 100644 --- a/src/main/java/io/github/bensku/tsbind/ast/TypeRef.java +++ b/src/main/java/io/github/bensku/tsbind/ast/TypeRef.java @@ -12,7 +12,7 @@ import com.github.javaparser.resolution.types.ResolvedType; public abstract class TypeRef implements AstNode { - + public static final Simple VOID = new Simple("void"); public static final Simple BOOLEAN = new Simple("boolean"); public static final Simple BYTE = new Simple("byte"); @@ -22,11 +22,11 @@ public abstract class TypeRef implements AstNode { public static final Simple LONG = new Simple("long"); public static final Simple FLOAT = new Simple("float"); public static final Simple DOUBLE = new Simple("double"); - + public static final Simple OBJECT = new Simple("java.lang.Object"); public static final Simple STRING = new Simple("java.lang.String"); public static final Simple LIST = new Simple("java.util.List"); - + public static TypeRef fromType(ResolvedType type, boolean nullable) { if (nullable) { return new Nullable(fromType(type)); @@ -34,7 +34,7 @@ public static TypeRef fromType(ResolvedType type, boolean nullable) { return fromType(type); } } - + public static TypeRef fromType(ResolvedType type) { if (type.isVoid()) { return VOID; @@ -84,7 +84,7 @@ public static TypeRef fromType(ResolvedType type) { throw new AssertionError("unexpected type: " + type); } } - + private static Simple getSimpleType(String name) { switch (name) { case "java.lang.Boolean": @@ -111,7 +111,7 @@ private static Simple getSimpleType(String name) { return new Simple(name); } } - + public static TypeRef fromDeclaration(ResolvedTypeParameterDeclaration decl) { if (decl.hasUpperBound()) { return new Parametrized(new Simple(decl.getName()), @@ -122,11 +122,11 @@ public static TypeRef fromDeclaration(ResolvedTypeParameterDeclaration decl) { return getSimpleType(decl.getName()); } } - + public static TypeRef enumSuperClass(TypeRef enumType) { return new Parametrized(getSimpleType("java.lang.Enum"), List.of(enumType)); } - + public static TypeRef fromDeclaration(String typeName, ResolvedReferenceTypeDeclaration decl) { var typeParams = decl.getTypeParameters(); if (typeParams.isEmpty()) { @@ -136,9 +136,9 @@ public static TypeRef fromDeclaration(String typeName, ResolvedReferenceTypeDecl return new Parametrized(getSimpleType(decl.getQualifiedName()), params); } } - + public static class Simple extends TypeRef { - + /** * Fully qualified name of the type, excluding array dimensions. */ @@ -152,7 +152,7 @@ private Simple(String name) { public String name() { return name; } - + @Override public TypeRef baseType() { return this; // Base of most types @@ -162,7 +162,7 @@ public TypeRef baseType() { public int arrayDimensions() { return 0; } - + @Override public void walk(Consumer visitor) { visitor.accept(this); @@ -181,7 +181,7 @@ public int hashCode() { return name.hashCode(); } } - + public static class Wildcard extends TypeRef { /** @@ -197,7 +197,7 @@ private Wildcard(TypeRef extendedType) { public String name() { return "*"; } - + @Override public TypeRef baseType() { return this; // Extended type is definitely not base type @@ -207,11 +207,11 @@ public TypeRef baseType() { public int arrayDimensions() { return 0; } - + public TypeRef extendedType() { return extendedType; } - + @Override public void walk(Consumer visitor) { visitor.accept(this); @@ -230,16 +230,16 @@ public boolean equals(Object obj) { public int hashCode() { return extendedType.hashCode(); } - + } - + public static class Parametrized extends TypeRef { - + /** * Base type that generic type parameters are applied to. */ private final TypeRef baseType; - + /** * Type parameters. */ @@ -254,7 +254,7 @@ private Parametrized(TypeRef baseType, List params) { public String name() { return baseType.name(); } - + @Override public TypeRef baseType() { return baseType.baseType(); @@ -264,11 +264,11 @@ public TypeRef baseType() { public int arrayDimensions() { return 0; } - + public List typeParams() { return params; } - + @Override public void walk(Consumer visitor) { visitor.accept(this); @@ -290,14 +290,14 @@ public int hashCode() { return baseType.hashCode() + 31 * params.hashCode(); } } - + public static class Array extends TypeRef { - + /** * Array component type. */ private final TypeRef component; - + /** * Array dimensions. */ @@ -312,7 +312,7 @@ private Array(TypeRef component, int dimensions) { public String name() { return component.name() + "[]".repeat(dimensions); } - + @Override public TypeRef baseType() { return component.baseType(); @@ -322,7 +322,7 @@ public TypeRef baseType() { public int arrayDimensions() { return dimensions; } - + @Override public void walk(Consumer visitor) { visitor.accept(this); @@ -343,18 +343,18 @@ public int hashCode() { return component.hashCode() + 31 * dimensions; } } - + public static class Nullable extends TypeRef { /** * Type that is nullable. */ private final TypeRef type; - + private Nullable(TypeRef type) { this.type = type; } - + /** * Type that we wrap because it is nullable. * @return Nullable type. @@ -362,7 +362,7 @@ private Nullable(TypeRef type) { public TypeRef nullableType() { return type; } - + @Override public String name() { return type.name(); @@ -377,7 +377,7 @@ public TypeRef baseType() { public int arrayDimensions() { return type.arrayDimensions(); } - + @Override public void walk(Consumer visitor) { type.walk(visitor); @@ -395,28 +395,33 @@ public boolean equals(Object obj) { public int hashCode() { return type.hashCode(); } - + } - + public abstract String name(); - + public String simpleName() { String name = name(); return name.substring(name.lastIndexOf('.') + 1); } - + public abstract TypeRef baseType(); - + public abstract int arrayDimensions(); - + public Array makeArray(int dimensions) { return new Array(this, dimensions); } - + @Override public abstract boolean equals(Object obj); - + @Override public abstract int hashCode(); - + + @Override + public String toString() { + return name(); + } + } diff --git a/src/main/java/io/github/bensku/tsbind/binding/BindingGenerator.java b/src/main/java/io/github/bensku/tsbind/binding/BindingGenerator.java index 8920c56..029fff4 100644 --- a/src/main/java/io/github/bensku/tsbind/binding/BindingGenerator.java +++ b/src/main/java/io/github/bensku/tsbind/binding/BindingGenerator.java @@ -1,9 +1,6 @@ package io.github.bensku.tsbind.binding; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Stream; import io.github.bensku.tsbind.AstConsumer; @@ -17,7 +14,7 @@ public class BindingGenerator implements AstConsumer { static final Set EXCLUDED_TYPES = new HashSet<>(); - + static { EXCLUDED_TYPES.add(TypeRef.BOOLEAN); EXCLUDED_TYPES.add(TypeRef.BYTE); @@ -30,30 +27,45 @@ public class BindingGenerator implements AstConsumer { EXCLUDED_TYPES.add(TypeRef.STRING); EXCLUDED_TYPES.add(TypeRef.OBJECT); } - + /** * Whether or not index.d.ts should be generated. */ private final boolean buildIndex; - - public BindingGenerator(boolean buildIndex) { + + private boolean emitReadOnly; + + private List excludeMethods; + + private boolean gettersAndSettersOff; + + private boolean groupByModule; + + public BindingGenerator(boolean buildIndex, boolean emitReadOnly, List excludeMethods, boolean gettersAndSettersOff, boolean groupByModule) { this.buildIndex = buildIndex; + this.emitReadOnly = emitReadOnly; + this.excludeMethods = excludeMethods; + this.gettersAndSettersOff = gettersAndSettersOff; + this.groupByModule = groupByModule; } - + @Override public Stream> consume(Map types) { Map modules = new HashMap<>(); - + types.values().forEach(type -> addType(modules, type)); - + // Put modules in declarations based on their base packages (tld.domain) Map outputs = new HashMap<>(); for (TsModule module : modules.values()) { String basePkg = getBasePkg(module.name()).replace('.', '_'); + if (groupByModule) { + basePkg = module.name(); + } StringBuilder out = outputs.computeIfAbsent(basePkg, key -> new StringBuilder()); module.write(types, out); } - + // If requested, generate index.d.ts that references other files if (buildIndex) { StringBuilder index = new StringBuilder("// auto-generated references to packages\n"); @@ -62,11 +74,11 @@ public Stream> consume(Map types) { } outputs.put("index", index); } - + return outputs.entrySet().stream().map(entry -> new Result<>(entry.getKey() + ".d.ts", entry.getValue().toString())); } - + private String getBasePkg(String name) { int tld = name.indexOf('.'); if (tld == -1) { @@ -78,21 +90,25 @@ private String getBasePkg(String name) { } return name.substring(0, domain); } - + private void addType(Map modules, TypeDefinition type) { if (EXCLUDED_TYPES.contains(type.ref)) { return; // Don't generate this type } - + // Get module for package the class is in, creating if needed - modules.computeIfAbsent(getModuleName(type.ref), TsModule::new).addType(type); - + modules.computeIfAbsent(getModuleName(type.ref), TsModule::new) + .addType(type) + .emitReadOnly(emitReadOnly) + .excludeMethods(excludeMethods) + .gettersAndSettersOff(gettersAndSettersOff); + // Fake inner classes with TS modules // Nested types in TS are quite different from Java, so we can't use them type.members.stream().filter(member -> (member instanceof TypeDefinition)) .forEach(innerType -> addType(modules, (TypeDefinition) innerType)); } - + private String getModuleName(TypeRef type) { // All parts except the last return type.name().substring(0, type.name().length() - type.simpleName().length() - 1); diff --git a/src/main/java/io/github/bensku/tsbind/binding/EarlyTypeTransformer.java b/src/main/java/io/github/bensku/tsbind/binding/EarlyTypeTransformer.java index 5a5679d..940bc46 100644 --- a/src/main/java/io/github/bensku/tsbind/binding/EarlyTypeTransformer.java +++ b/src/main/java/io/github/bensku/tsbind/binding/EarlyTypeTransformer.java @@ -1,9 +1,9 @@ package io.github.bensku.tsbind.binding; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import io.github.bensku.tsbind.ast.Member; import io.github.bensku.tsbind.ast.Method; @@ -13,16 +13,19 @@ /** * Performs early type transformations. They are required e.g. when the pass * might add new used types that could affect imports. - * + * * Early transform passes can and will mutate the contents of types! * */ public class EarlyTypeTransformer { - + private final Map typeTable; - - public EarlyTypeTransformer(Map typeTable) { + + private final List methodWhiteListPatterns; + + public EarlyTypeTransformer(Map typeTable, List methodWhitelist) { this.typeTable = typeTable; + this.methodWhiteListPatterns = methodWhitelist.stream().map(Pattern::compile).collect(Collectors.toList()); } private void visitSupertypes(TypeDefinition type, Consumer visitor) { @@ -42,7 +45,7 @@ private void visitSupertypes(TypeDefinition type, Consumer visit } } } - + /** * TypeScript removes inherited overloads unless they're re-specified. * As such, we copy them to classes that should inherit them. @@ -55,18 +58,86 @@ public void addMissingOverloads(TypeDefinition type) { methods.add(new MethodId((Method) member)); } } - + // Visit supertypes and interfaces to see what we're missing visitSupertypes(type, parent -> { - for (Member member : parent.members) { - if (member instanceof Method && type.hasMember(member.name())) { + for (Member parentMember : parent.members) { + if (parentMember instanceof Method && type.hasMember(parentMember.name())) { // We have a member with same name // If it has different signature, we need to copy the missing overload - if (!methods.contains(new MethodId((Method) member))) { - type.members.add(member); + MethodId parentMethodId = new MethodId((Method) parentMember); + if (!methods.contains(parentMethodId)) { + // now we must check if the parent method is allowed by the whitelist + if (methodWhiteListPatterns.stream().anyMatch(p -> p.matcher(type.name() + "." + parentMethodId.name).matches())) { + type.members.add(parentMember); + } + + type.members.add(parentMember); } } } }); } + + public void flattenType(TypeDefinition type) { + // Figure out what methods we already have + Set typeMethodIds = new HashSet<>(); + for (Member member : type.members) { + if (member instanceof Method) { + typeMethodIds.add(new MethodId((Method) member)); + } + } + List superTypesToRemove = new ArrayList<>(); + visitSupertypes(type, parent -> { + for (Member parentMember : parent.members) { + if (parentMember instanceof Method) { + // If it has different signature, we need to copy the missing overload + MethodId parentMethodId = new MethodId((Method) parentMember); + if (!typeMethodIds.contains(parentMethodId)) { + // now we must check if the parent method is allowed by the whitelist + if (methodWhiteListPatterns.stream().anyMatch(p -> p.matcher(type.name() + "." + parentMethodId.name).matches())) { + type.members.add(parentMember); + } + } + } + } + // now we must remove the parent from the type's superTypes + superTypesToRemove.add(parent.ref); + }); + type.superTypes.removeAll(superTypesToRemove); + type.interfaces.removeAll(superTypesToRemove); + } + + public void forceParentJavadocs(TypeDefinition type) { + // Figure out what methods we already have + Set typeMethodIds = new HashSet<>(); + for (Member typeMember : type.members) { + if (typeMember instanceof Method) { + typeMethodIds.add(new MethodId((Method) typeMember)); + } + } + visitSupertypes(type, parent -> { + for (Member parentMember : parent.members) { + if (parentMember instanceof Method) { + MethodId parentMethodId = new MethodId((Method) parentMember); + if (typeMethodIds.contains(parentMethodId)) { + // we now need to find the corresponding method by its method it in the type members and copy + // the javadoc from the parent only if the type's one is empty or only contains @inheritDoc + for (Member typeMember : type.members) { + if (typeMember instanceof Method) { + MethodId typeMethodId = new MethodId((Method) typeMember); + if (typeMethodId.equals(parentMethodId)) { + Method typeMethod = (Method) typeMember; + if (typeMethod.javadoc.isEmpty() || typeMethod.javadoc.get().trim().equals("@inheritDoc")) { + typeMethod.javadoc = parentMember.javadoc; + } + } + } + } + } + } + } + }); + } + } diff --git a/src/main/java/io/github/bensku/tsbind/binding/TsClass.java b/src/main/java/io/github/bensku/tsbind/binding/TsClass.java index 8dc9267..d56c30c 100644 --- a/src/main/java/io/github/bensku/tsbind/binding/TsClass.java +++ b/src/main/java/io/github/bensku/tsbind/binding/TsClass.java @@ -82,7 +82,7 @@ private Optional resolveOverrideSource(TypeRef type, Method meth /** * Finds an interface method that the given method overrides. - * @param member Method to find overrides for. + * @param method Method to find overrides for. * @return Overridden member, if found. */ private Optional resolveInterfaceOverride(Method method) { @@ -165,7 +165,7 @@ private void invalidateGetSet(int index) { if (member instanceof Getter || member instanceof Setter) { Method original = (Method) member; Method method = new Method(original.originalName(), original.returnType, original.params, - original.typeParams, original.javadoc.orElse(null), original.isPublic, original.isStatic, original.isOverride); + original.typeParams, original.javadoc.orElse(null), original.isPublic, original.isStatic, original.isOverride, original.typeName); members.set(index, method); } // other kinds of conflicts we don't touch } @@ -227,23 +227,55 @@ public void emit(TypeDefinition node, TsEmitter out) { return; } } - - out.print("export class "); - emitName(node.ref.simpleName(), node.ref, out); - - // We can't use TS 'implements', because TS interfaces - // don't support e.g getters/setters - // Instead, we translate Java implements to TS extends - List superTypes = new ArrayList<>(node.superTypes); - superTypes.addAll(node.interfaces); + boolean mixinTrick = false; - if (!superTypes.isEmpty()) { - // At least one supertype; there may be more, but we'll use mixin trick for that - // (we still want to extend even just one type to get @inheritDoc) - out.print(" extends %s", superTypes.get(0)); - } - if (superTypes.size() > 1) { - mixinTrick = true; // Trick multiple inheritance + boolean inInterface = false; + if (out.isUseGettersAndSetters()) { + out.print("export class "); + emitName(node.ref.simpleName(), node.ref, out); + + // We can't use TS 'implements', because TS interfaces + // don't support e.g getters/setters + // Instead, we translate Java implements to TS extends + List superTypes = new ArrayList<>(node.superTypes); + superTypes.addAll(node.interfaces); + if (!superTypes.isEmpty()) { + // At least one supertype; there may be more, but we'll use mixin trick for that + // (we still want to extend even just one type to get @inheritDoc) + out.print(" extends %s", superTypes.get(0)); + } + if (superTypes.size() > 1) { + mixinTrick = true; // Trick multiple inheritance + } + } else { + String kind = node.kind.toString().toLowerCase(); + if (node.kind == TypeDefinition.Kind.ENUM) { + // we map enums to classes in TS + kind = "class"; + } + out.print("export " + kind + " "); + emitName(node.ref.simpleName(), node.ref, out); + if (node.kind == TypeDefinition.Kind.CLASS || node.kind == TypeDefinition.Kind.ENUM) { + List superTypes = new ArrayList<>(node.superTypes); + if (!superTypes.isEmpty()) { + out.print(" extends "); + out.print(superTypes,", "); + } + List interfaces = new ArrayList<>(node.interfaces); + if (!interfaces.isEmpty()) { + out.print(" implements "); + out.print(interfaces, ", "); + } + } else if (node.kind == TypeDefinition.Kind.INTERFACE) { + inInterface = true; + List superTypes = new ArrayList<>(node.superTypes); + if (!superTypes.isEmpty()) { + out.print(" extends "); + out.print(superTypes,", "); + } + } else { + throw new AssertionError("Unknown type kind: " + node.kind); + } } out.println(" {"); @@ -273,6 +305,8 @@ public void emit(TypeDefinition node, TsEmitter out) { emitName(node.ref.simpleName(), node.ref, out); out.print(" extends "); // FIXME quick hack to get List -> array conversion out of supertypes + List superTypes = new ArrayList<>(node.superTypes); + superTypes.addAll(node.interfaces); out.print(superTypes.stream() .filter(type -> !type.baseType().equals(TypeRef.LIST)).collect(Collectors.toList()), ", "); out.println(" {}"); diff --git a/src/main/java/io/github/bensku/tsbind/binding/TsEmitter.java b/src/main/java/io/github/bensku/tsbind/binding/TsEmitter.java index 9bc49e8..baa9f22 100644 --- a/src/main/java/io/github/bensku/tsbind/binding/TsEmitter.java +++ b/src/main/java/io/github/bensku/tsbind/binding/TsEmitter.java @@ -1,9 +1,7 @@ package io.github.bensku.tsbind.binding; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.regex.Pattern; import org.jsoup.Jsoup; @@ -69,7 +67,15 @@ public void close() { */ private final Map types; - public TsEmitter(String indentation, Map typeNames, Map types) { + private boolean emitReadOnly = false; + + private List excludeMethodPatterns = new ArrayList<>(); + + private boolean useGettersAndSetters; + + private boolean lastPrintSkipped = false; + + public TsEmitter(String indentation, Map typeNames, Map types, boolean emitReadonly, List excludeMethods, boolean useGettersAndSetters) { this.output = new StringBuilder(); this.indentation = indentation; this.indenter = new Indenter(); @@ -77,6 +83,13 @@ public TsEmitter(String indentation, Map typeNames, Map(); this.types = types; + this.emitReadOnly = emitReadonly; + this.useGettersAndSetters = useGettersAndSetters; + if (excludeMethods != null) { + for (String pattern : excludeMethods) { + excludeMethodPatterns.add(Pattern.compile(pattern)); + } + } registerGenerators(); } @@ -156,7 +169,24 @@ public TsEmitter print(AstNode node) { if (generator == null) { throw new UnsupportedOperationException("unsupported node type " + node.getClass()); } + if (emitReadOnly && + (node instanceof Constructor || node instanceof Setter)) { + lastPrintSkipped = true; + return this; + } + if (!excludeMethodPatterns.isEmpty()) { + if (node instanceof Method) { + Method method = (Method) node; + for (Pattern pattern : excludeMethodPatterns) { + if (pattern.matcher(method.typeName() + "." + method.name()).matches()) { + lastPrintSkipped = true; + return this; + } + } + } + } generator.emit(node, this); + lastPrintSkipped = false; return this; } @@ -168,8 +198,10 @@ public TsEmitter println(AstNode node) { public TsEmitter print(List list, String delimiter) { for (int i = 0; i < list.size() - 1; i++) { print(list.get(i)); + if (!lastPrintSkipped) { output.append(delimiter); } + } if (!list.isEmpty()) { print(list.get(list.size() - 1)); } @@ -229,4 +261,8 @@ public TsEmitter printType(TypeRef type) { public String toString() { return output.toString(); } + + public boolean isUseGettersAndSetters() { + return useGettersAndSetters; + } } diff --git a/src/main/java/io/github/bensku/tsbind/binding/TsModule.java b/src/main/java/io/github/bensku/tsbind/binding/TsModule.java index 2049fb4..da529f3 100644 --- a/src/main/java/io/github/bensku/tsbind/binding/TsModule.java +++ b/src/main/java/io/github/bensku/tsbind/binding/TsModule.java @@ -18,28 +18,50 @@ public class TsModule { * be fully qualified name of the outer class instead. */ private final String name; - + + private boolean emitReadOnly = false; + + private List excludeMethods = new ArrayList<>(); + + private boolean useGettersAndSetters = false; + /** * Types in this module. */ private final List types; - + public TsModule(String name) { this.name = name; this.types = new ArrayList<>(); } - + public String name() { return name; } - - public void addType(TypeDefinition type) { + + public TsModule addType(TypeDefinition type) { types.add(type); + return this; + } + + public TsModule emitReadOnly(boolean emitReadOnly) { + this.emitReadOnly = emitReadOnly; + return this; + } + + public TsModule excludeMethods(List excludeMethods) { + this.excludeMethods = excludeMethods; + return this; } - + + public TsModule gettersAndSettersOff(boolean useGettersAndSetters) { + this.useGettersAndSetters = useGettersAndSetters; + return this; + } + public void write(Map typeTable, StringBuilder sb) { sb.append("declare module '").append(name).append("' {\n"); - + class Import { /** * Module name to import from. @@ -50,13 +72,13 @@ class Import { * should be used in this module. In many cases, these are equal. */ final Map names; - + Import(String from) { this.from = from; this.names = new HashMap<>(); } } - + // Figure out type names and import declarations Map typeNames = findTypeNames(); Map imports = new HashMap<>(); @@ -67,7 +89,7 @@ class Import { imports.computeIfAbsent(from, n -> new Import(from)).names.put(type.simpleName(), name); } }); - + // Emit import lines for (Import line : imports.values()) { sb.append("import { "); @@ -80,15 +102,15 @@ class Import { }).collect(Collectors.joining(", "))); sb.append(" } from '").append(line.from).append("';\n"); } - + // Generate classes of this module - TsEmitter emitter = new TsEmitter(" ", typeNames, typeTable); + TsEmitter emitter = new TsEmitter(" ", typeNames, typeTable, emitReadOnly, excludeMethods, useGettersAndSetters); types.forEach(emitter::print); sb.append(emitter.toString()); - + sb.append("\n}\n"); } - + private Map findTypeNames() { Map typeNames = new HashMap<>(); Set simpleNames = new HashSet<>(); @@ -109,7 +131,7 @@ private Map findTypeNames() { if (typeNames.containsKey(type)) { return; // Same type used again, this is fine } - + // On name collision, fall back to fully qualified names String simple = type.simpleName(); if (simpleNames.contains(simple)) { diff --git a/src/main/java/io/github/bensku/tsbind/cli/Args.java b/src/main/java/io/github/bensku/tsbind/cli/Args.java index 8d228f2..df1a603 100644 --- a/src/main/java/io/github/bensku/tsbind/cli/Args.java +++ b/src/main/java/io/github/bensku/tsbind/cli/Args.java @@ -15,48 +15,79 @@ public class Args { public enum OutputFormat { JSON((args) -> new JsonEmitter()), - TS_TYPES((args) -> new BindingGenerator(args.index)); - + TS_TYPES((args) -> new BindingGenerator(args.index, args.emitReadOnly, args.excludeMethods, args.gettersAndSettersOff, args.groupByModule)); + public final Function> consumerSource; - + OutputFormat(Function> consumer) { this.consumerSource = consumer; } } - + @Parameter(names = "--format") public OutputFormat format = OutputFormat.TS_TYPES; - + @Parameter(names = "--in") public List in; - + @Parameter(names = "--symbols") public List symbols = new ArrayList<>(); - + @Parameter(names = "--repo") public List repos = new ArrayList<>(); @Parameter(names = "--artifact") public List artifacts = new ArrayList<>(); - + @Parameter(names = "--offset") public String offset = ""; - + @Parameter(names = "--include") public List include = List.of(""); - + @Parameter(names = "--exclude") public List exclude = List.of(); - + @Parameter(names = "--blacklist") public List blacklist = List.of(); - + + @Parameter(names = "--methodWhitelist") + public List methodWhitelist = List.of(); + + @Parameter(names = "--fieldWhitelist") + public List fieldWhitelist = List.of(); + + @Parameter(names = "--gettersAndSettersOff") + public boolean gettersAndSettersOff; + @Parameter(names = "--out") public Path out = Path.of(""); - + @Parameter(names = "--packageJson") public Path packageJson; - + @Parameter(names = "--index") public boolean index; + + @Parameter(names = "--emitReadOnly") + public boolean emitReadOnly; + + @Parameter(names = "--excludeMethods") + public List excludeMethods = List.of(); + + @Parameter(names = "--groupByModule") + public boolean groupByModule; + + @Parameter(names = "--flattenTypes") + public boolean flattenTypes; + + @Parameter(names = "--forceParentJavadocs") + public boolean forceParentJavadocs; + + @Parameter(names = "--rootTypes") + public List rootTypes = List.of(); + + @Parameter(names = "--debugMatching") + public boolean debugMatching; + } diff --git a/src/main/java/io/github/bensku/tsbind/cli/BindGenApp.java b/src/main/java/io/github/bensku/tsbind/cli/BindGenApp.java index 93fbe73..730f139 100644 --- a/src/main/java/io/github/bensku/tsbind/cli/BindGenApp.java +++ b/src/main/java/io/github/bensku/tsbind/cli/BindGenApp.java @@ -5,11 +5,7 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Stream; import com.beust.jcommander.JCommander; @@ -29,15 +25,16 @@ import io.github.bensku.tsbind.AstGenerator; import io.github.bensku.tsbind.SourceUnit; import io.github.bensku.tsbind.ast.TypeDefinition; +import io.github.bensku.tsbind.ast.TypeRef; import io.github.bensku.tsbind.binding.EarlyTypeTransformer; public class BindGenApp { - + public static void main(String... argv) throws IOException, InterruptedException { // Parse command-line arguments Args args = new Args(); JCommander.newBuilder().addObject(args).build().parse(argv); - + if (args.packageJson != null) { args = new GsonBuilder() .registerTypeAdapter(Path.class, new TypeAdapter() { @@ -58,13 +55,13 @@ public Path read(JsonReader in) throws IOException { throw new IllegalArgumentException("missing tsbindOptions in --packageJson"); } } - + // Download the --artifact from Maven if provided List inputPaths; if (!args.artifacts.isEmpty()) { MavenResolver resolver = new MavenResolver(Files.createTempDirectory("tsbind"), args.repos); args.repos.add("https://repo1.maven.org/maven2"); // Maven central as last resort - + // Add all artifacts to input paths and symbols inputPaths = new ArrayList<>(); for (String artifact : args.artifacts) { @@ -77,17 +74,17 @@ public Path read(JsonReader in) throws IOException { inputPaths = args.in; } System.out.println("Generating types for " + inputPaths + " to " + args.out); - + // Prepare for AST generation JavaParser parser = setupParser(args.symbols); - AstGenerator astGenerator = new AstGenerator(parser, args.blacklist); - + AstGenerator astGenerator = new AstGenerator(parser, args.blacklist, args.methodWhitelist, args.fieldWhitelist, args.gettersAndSettersOff, args.debugMatching); + // Walk over input Java source files String offset = args.offset; List include = args.include; List exclude = args.exclude; Path outDir = args.out; - + try (Stream files = inputPaths.stream().map(t -> { if (Files.isDirectory(t)) { return t; @@ -115,7 +112,7 @@ public Path read(JsonReader in) throws IOException { .filter(Files::isRegularFile) .filter(f -> f.getFileName().toString().endsWith(".java")) .filter(f -> !f.getFileName().toString().equals("package-info.java"))) { - Map types = new HashMap<>(); + Map types = new TreeMap<>(); files.map(path -> { try { return new SourceUnit(path.toString(), Files.readString(path)); @@ -128,13 +125,49 @@ public Path read(JsonReader in) throws IOException { System.out.println("Parsed type " + type.name()); types.put(type.name(), type); }); - + // Apply early transformation passes that need all types - EarlyTypeTransformer earlyTransform = new EarlyTypeTransformer(types); + EarlyTypeTransformer earlyTransform = new EarlyTypeTransformer(types, args.methodWhitelist); for (TypeDefinition def : types.values()) { earlyTransform.addMissingOverloads(def); + } + if (args.forceParentJavadocs) { + for (TypeDefinition def : types.values()) { + earlyTransform.forceParentJavadocs(def); + } + } + if (args.flattenTypes) { + for (TypeDefinition def : types.values()) { + earlyTransform.flattenType(def); + } } - + + if (!args.rootTypes.isEmpty()) { + // We search all the accessible types and keep only the ones accessible through the root types + Set accessibleTypes = new TreeSet<>(args.rootTypes); + int oldAccessibleTypeSize = 0; + // We loop here because we need to keep adding types until we don't add any more + while (oldAccessibleTypeSize < accessibleTypes.size()) { + oldAccessibleTypeSize = accessibleTypes.size(); + for (TypeDefinition def : types.values()) { + if (accessibleTypes.contains(def.name())) { + accessibleTypes.add(def.name()); + def.walk(node -> { + if (node instanceof TypeDefinition) { + accessibleTypes.add(((TypeDefinition) node).name()); + } else if (node instanceof TypeRef) { + accessibleTypes.add(((TypeRef) node).name()); + } + }); + } + } + } + + // then we purge all the types that are not accessible from the root types + types.keySet().removeIf(key -> !accessibleTypes.contains(key)); + + } + Stream> results = args.format.consumerSource.apply(args) .consume(types); results.forEach(result -> { @@ -148,7 +181,7 @@ public Path read(JsonReader in) throws IOException { }); } } - + private static boolean isIncluded(String name, List includes, List excludes) { boolean include = false; for (String prefix : includes) { @@ -167,14 +200,14 @@ private static boolean isIncluded(String name, List includes, List symbolSources) throws IOException { CombinedTypeSolver typeSolver = new CombinedTypeSolver(); typeSolver.add(new ReflectionTypeSolver()); for (Path jar : symbolSources) { typeSolver.add(new JarTypeSolver(jar)); } - + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver); ParserConfiguration config = new ParserConfiguration(); config.setLanguageLevel(LanguageLevel.JAVA_16);