From 10f9150d90034894ac63d8c4b2e52164c70a2d48 Mon Sep 17 00:00:00 2001 From: Simon Seyock Date: Fri, 20 Dec 2024 16:26:53 +0100 Subject: [PATCH] feat: further improve output --- README.md | 66 ++++---- config/j2ts-config.json | 4 + config/tsconfig.json | 4 +- src/main/scala/java2typescript/main.scala | 23 ++- .../transformer/builtins.scala | 61 +++++++ .../java2typescript/transformer/classes.scala | 24 ++- .../transformer/contexts.scala | 33 +++- .../transformer/expressions.scala | 36 +--- .../transformer/statements.scala | 3 +- .../transformer/transform.scala | 28 +++- .../transformer/unsupported/transform.scala | 17 +- .../resources/fixtures/anonymous-classes.md | 37 +++++ src/test/resources/fixtures/array.md | 4 +- src/test/resources/fixtures/assertions.md | 2 +- src/test/resources/fixtures/assignments.md | 4 +- src/test/resources/fixtures/calls.md | 4 +- src/test/resources/fixtures/classes.md | 57 ++++++- .../resources/fixtures/control-structures.md | 34 ++-- src/test/resources/fixtures/data-types.md | 31 ++++ src/test/resources/fixtures/enum.md | 13 ++ src/test/resources/fixtures/expressions.md | 12 +- .../indexed-point-in-area-locator.md | 155 ++++++++++-------- .../fixtures/properties-parameters.md | 32 ++++ .../fixtures/throw-try-catch-finally.md | 6 +- 24 files changed, 487 insertions(+), 203 deletions(-) create mode 100644 src/main/scala/java2typescript/transformer/builtins.scala create mode 100644 src/test/resources/fixtures/anonymous-classes.md create mode 100644 src/test/resources/fixtures/data-types.md diff --git a/README.md b/README.md index 5a1bdff..030c9a7 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,10 @@ class A { the java class to convert } ``` -``` +```typescript +class A { + the typescript code that should be generated +} ``` ```` @@ -55,7 +58,7 @@ if you want to debug a fixture, you can add the line `options: debug` right unde options: debug ``` -This will cause the `FixtureSpec` test to only run this single fixture and allways fail (so it won't pass the ci). You can then debug the `FixtureSpec` test and set a breakpoint anywhere. +This will cause the `FixtureSpec` test to only run this single fixture and always fail (so it won't pass the ci). You can then debug the `FixtureSpec` test and set a breakpoint anywhere. ### Run program @@ -79,41 +82,36 @@ If you want to see how the typescript AST looks, you can use https://ts-ast-view * Find enum `SyntaxKind` in `typescript.d.ts` (`node_modules/@ts-morph/common/lib/typescript.d.ts`) * Convert enum to scala in `java2typescript/ast/SyntaxKind.scala` -## TODOs +## How does it work? -* add a first parsing step that only determines exports and maps them - * add imports for classes from the same package - * add different imports for nested classes - * add different imports for static methods on enums -* add package.json -* add tsconfig -* create drop in replacements for java builtins +The program runs in multiple steps: -parseExports +1. It reads a config file that contains context information about the code that should be translated. An example lives in `config/j2ts-config.json`. +2. It gathers all configured files and reads them into memory. It uses regex replacements from the config file to replace some java constructs with other constructs. +3. It analyzes all exports. Later in the code types that are used are checked against this list via `context.addImportIfNeeded`. +4. It creates a project context that contains this information +5. It parses all files one-by-one and creates a typescript ast out of it +6. It takes the ast for a file and starts a javascript program using the typescript compiler to convert the file into typescript code. +7. It writes the generated typescript code into the target files -export -> import -class XY -> import {XY} from "filename" -in code -new XY or XY.staticMethod (same) - -class XY { class AB } --> -class XY {} -class XY_AB {} --> -import {XY, AB} from "filename" - -in code -new XY.AB --> -new XY_AB +## TODOs -enum XY { static something() } --> -enum XY {} -XY_something() +* CURRENT: Point.java / Geometry.java +* Improve documentation +* methods from inherited classes need to be prefixed by this + * At the moment it works like this: for a given name the program checks if the name is a parameter of a method or a property of the class, if yes it prepends this. + * this is not sufficient for names from parent class. if we would gather all local names (names in the class context, parameters and any local variables) we could determine this correctly. +* Find a clever way to transform constructs that work in Java into equivalent TypeScript structures +* super is called +* create drop in replacements for java builtins + * List, ArrayList, Collection -> array +* Automatically filter files that contain un-translatable structures + * Disabled at the moment + * Is done on class level at the moment -> move down to method level + * replace by error throwing constructs +* What to do with System.getProperty? +* Check if enum construct is sufficient +* Improve performance of the typescript code generation step +* Use const instead of let if possible --> import {XY, something} from "filename" -in code -XY.something -> something diff --git a/config/j2ts-config.json b/config/j2ts-config.json index ddbb56b..719ba86 100644 --- a/config/j2ts-config.json +++ b/config/j2ts-config.json @@ -31,6 +31,10 @@ { "pattern": "public Object clone\\(\\)(\\s+\\{\\s*return copy\\(\\);\\s*}|;)", "replacement": "" + }, + { + "pattern": "private static final long serialVersionUID = \\d+L;", + "replacement": "" } ], "skipFiles": [ diff --git a/config/tsconfig.json b/config/tsconfig.json index 63eda29..94ec9b8 100644 --- a/config/tsconfig.json +++ b/config/tsconfig.json @@ -75,10 +75,10 @@ /* Type Checking */ "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ - // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ + "strictNullChecks": false, /* When type checking, take into account `null` and `undefined`. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ - // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */ // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ diff --git a/src/main/scala/java2typescript/main.scala b/src/main/scala/java2typescript/main.scala index fd7164d..4b994c4 100644 --- a/src/main/scala/java2typescript/main.scala +++ b/src/main/scala/java2typescript/main.scala @@ -9,6 +9,7 @@ import java.io.File import java.nio.file.{Files, Path, Paths} import scala.io.Source import scala.jdk.CollectionConverters.* +import scala.util.Using import scala.util.matching.Regex @main def main(configFile: String, files: String*): Unit = @@ -35,15 +36,20 @@ import scala.util.matching.Regex val contents = readFiles(config, gatheredFiles) println("analyse exports") + val analyseProgress = Progress(contents.length) val importMappings = contents.flatMap { - (file, content) => analyseExports(content) + (file, content) => + analyseProgress.step() + analyseExports(content) } val context = ProjectContext(config, importMappings) println("parse files") + val parseProgress = Progress(contents.length) val parseResults = contents.flatMap { (file, content) => + parseProgress.step() try Some(file, parser.parse(context, content)) catch case err: Throwable => println(s"error occured in $file") @@ -52,8 +58,10 @@ import scala.util.matching.Regex } println("create typescript code") + val tsProgress = Progress(parseResults.length) val tsContents = parseResults.flatMap { (file, parseResult) => + tsProgress.step() try Some(file, writer.write(parseResult)) catch case err: Throwable => println(s"error occured in $file") @@ -62,8 +70,10 @@ import scala.util.matching.Regex } println("write files") + val writeProgress = Progress(tsContents.length) tsContents.foreach { (file, tsContent) => { + writeProgress.step() val target = changeFileExtension(targetPath.resolve(sourcePath.relativize(file)), "ts") val fileWriter = new java.io.PrintWriter(target.toFile) try @@ -140,3 +150,14 @@ def readFiles(config: Config, files: List[Path]): List[(Path, String)] = file => (file, replace(readFile(file.toFile), config.replacements)) } + +class Progress (val total: Int, val barLength: Int = 50) { + var count = 0 + def step(): Unit = { + val filled = (count.toFloat / total * barLength).toInt + print(s"\r[${"=".repeat(filled)}>${" ".repeat(barLength - filled - 1)}]") + count += 1 + if (count == total) + println("") + } +} diff --git a/src/main/scala/java2typescript/transformer/builtins.scala b/src/main/scala/java2typescript/transformer/builtins.scala new file mode 100644 index 0000000..8865a2a --- /dev/null +++ b/src/main/scala/java2typescript/transformer/builtins.scala @@ -0,0 +1,61 @@ +package java2typescript.transformer + +import com.github.javaparser.ast.expr.{ObjectCreationExpr, SimpleName} +import java2typescript.ast + +import scala.jdk.CollectionConverters.* + +def transformObjectCreationExpression(context: ParameterContext, expr: ObjectCreationExpr): ast.Expression = + if (isExceptionType(expr)) + transformExceptionCreationExpression(context, expr) + else if (isArrayType(expr)) + transformArrayExpression(context, expr) + else + context.addImportIfNeeded(getTypeScope(expr.getType), getTypeName(expr.getType)) + ast.NewExpression( + ast.Identifier(expr.getType.getName.getIdentifier), + transformArguments(context, expr.getArguments), + transformTypeArguments(context, expr.getTypeArguments) + ) + +def isExceptionType(expr: ObjectCreationExpr): Boolean = + getTypeName(expr.getType).endsWith("Exception") || getTypeName(expr.getType).endsWith("Error") + +def transformExceptionCreationExpression(context: ParameterContext, expr: ObjectCreationExpr): ast.Expression = + val name = expr.getType.getName + if (name.asString() == "Error") + return ast.NewExpression( + ast.Identifier("Error"), + transformArguments(context, expr.getArguments), + transformTypeArguments(context, expr.getTypeArguments) + ) + val args = expr.getArguments.asScala + if (args.length > 1) + println(s"WARN: ${expr.getType.getName}: all but the first error argument are dropped.") + + ast.NewExpression( + ast.Identifier("Error"), + List( + if (args.isEmpty) + ast.StringLiteral(name.toString) + else + ast.BinaryExpression( + ast.StringLiteral(s"$name: "), + transformExpression(context, args.head), + ast.PlusToken() + ) + ) + ) + +def isArrayType(expr: ObjectCreationExpr): Boolean = + val typeName = getTypeName(expr.getType) + typeName.endsWith("List") || typeName == "Collection" + +def transformArrayExpression(context: ParameterContext, expr: ObjectCreationExpr) = + ast.ArrayLiteralExpression(List()) + +def isBuiltInType(name: SimpleName): Boolean = + List("Math", "System").contains(name.asString) + +def isDroppableInterface(name: SimpleName): Boolean = + List("Cloneable", "Comparable", "Serializable").contains(name.asString) \ No newline at end of file diff --git a/src/main/scala/java2typescript/transformer/classes.scala b/src/main/scala/java2typescript/transformer/classes.scala index 8052de3..b157e83 100644 --- a/src/main/scala/java2typescript/transformer/classes.scala +++ b/src/main/scala/java2typescript/transformer/classes.scala @@ -5,18 +5,19 @@ import com.github.javaparser.ast.{Modifier, NodeList} import com.github.javaparser.ast.`type`.ClassOrInterfaceType import com.github.javaparser.ast.body.{BodyDeclaration, ClassOrInterfaceDeclaration, ConstructorDeclaration, EnumDeclaration, FieldDeclaration, InitializerDeclaration, MethodDeclaration, Parameter} import java2typescript.ast +import java2typescript.ast.SyntaxKind import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* -def transformHeritage(context: FileContext, nodes: NodeList[ClassOrInterfaceType], token: ast.SyntaxKind): Option[ast.HeritageClause] = - val implementedTypes = nodes.asScala.toList - if (implementedTypes.nonEmpty) +def transformHeritage(context: FileContext, ancestors: List[ClassOrInterfaceType], token: ast.SyntaxKind): Option[ast.HeritageClause] = + if (ancestors.nonEmpty) Some( ast.HeritageClause( - implementedTypes.map { + ancestors.map { t => + context.addImportIfNeeded(None, t.getName.asString) ast.ExpressionWithTypeArguments( transformName(t.getName), t.getTypeArguments.toScala.toList @@ -56,6 +57,7 @@ def transformClassOrInterfaceDeclaration( val classContext = context match case c: FileContext => ClassContext(c, Some(decl)) case c: ClassContext => ClassContext(c, Some(decl), Option(c)) + val members = decl.getMembers.asScala .flatMap(transformMember.curried(classContext)) @@ -70,8 +72,8 @@ def transformClassOrInterfaceDeclaration( transformName(decl.getName), members = members.toList, modifiers = modifiersVal ::: List(ast.AbstractKeyword()), - heritageClauses = transformHeritage(context, decl.getExtendedTypes, ast.SyntaxKind.ExtendsKeyword).toList - ::: transformHeritage(context, decl.getImplementedTypes, ast.SyntaxKind.ImplementsKeyword).toList + heritageClauses = transformHeritage(context, decl.getExtendedTypes.asScala.toList, ast.SyntaxKind.ExtendsKeyword).toList + ::: transformHeritage(context, decl.getImplementedTypes.asScala.filter(t => !isDroppableInterface(t.getName)).toList, ast.SyntaxKind.ImplementsKeyword).toList ) :: extractedStatements @@ -110,8 +112,8 @@ def transformClassOrInterfaceDeclaration( transformName(decl.getName), members = properties ::: constructorsWithOverloads ::: methodsWithOverloads, modifiers = modifiersVal, - heritageClauses = transformHeritage(context, decl.getExtendedTypes, ast.SyntaxKind.ExtendsKeyword).toList - ::: transformHeritage(context, decl.getImplementedTypes, ast.SyntaxKind.ImplementsKeyword).toList + heritageClauses = transformHeritage(context, decl.getExtendedTypes.asScala.toList, ast.SyntaxKind.ExtendsKeyword).toList + ::: transformHeritage(context, decl.getImplementedTypes.asScala.filter(t => !isDroppableInterface(t.getName)).toList, ast.SyntaxKind.ImplementsKeyword).toList ) :: extractedStatements @@ -209,7 +211,11 @@ def transformMethodDeclaration(context: ClassContext, decl: MethodDeclaration) = val methodBody = decl.getBody.toScala.map(body => ast.Block(body.getStatements.asScala.map(transformStatement.curried(methodContext)).toList) ) - val methodModifiers = decl.getModifiers.asScala.flatMap(transformModifier).toList + val originalMethodModifiers = decl.getModifiers.asScala.flatMap(transformModifier).toList + val methodModifiers = if (originalMethodModifiers.isEmpty) + List(ast.PublicKeyword(), ast.AbstractKeyword()) + else + originalMethodModifiers ast.MethodDeclaration( transformName(decl.getName), diff --git a/src/main/scala/java2typescript/transformer/contexts.scala b/src/main/scala/java2typescript/transformer/contexts.scala index 0c39e7b..d20f261 100644 --- a/src/main/scala/java2typescript/transformer/contexts.scala +++ b/src/main/scala/java2typescript/transformer/contexts.scala @@ -26,10 +26,18 @@ class FileContext( val projectContext: ProjectContext, val packageName: Option[String], val extractedExport: mutable.Buffer[Import] = ListBuffer(), - val neededImports: mutable.Buffer[Import] = ListBuffer() + val neededImports: mutable.Buffer[Import] = ListBuffer(), + val localIdentifiers: mutable.Buffer[ast.Identifier] = ListBuffer() ) extends ProjectContext(projectContext.config, projectContext.importMappings) { + def addLocalName(identifier: ast.Identifier): Unit = + localIdentifiers += identifier + + def isLocal(name: SimpleName): Boolean = + localIdentifiers.exists(i => i.escapedText == name.getIdentifier) + def addExtractedExport(imp: Import): Unit = extractedExport += imp + def addImportIfNeeded(scope: Option[String], name: String): Unit = if (isImportable(scope, name)) val exists = neededImports.exists { @@ -45,10 +53,13 @@ class FileContext( else addImportIfNeeded(Some(splitted.dropRight(1).mkString(".")), splitted(-1)) } + def getImport(scope: Option[String], name: String): Option[Import] = importMappings.find { im => im.javaScope == scope && im.javaName == name } + def isImportedName(name: SimpleName): Boolean = + neededImports.exists(p => p.javaName == name.asString) } class ClassContext( @@ -56,7 +67,7 @@ class ClassContext( val classOrInterface: Option[ClassOrInterfaceDeclaration|EnumDeclaration], val parentClassContext: Option[ClassContext] = Option.empty, val extractedStatements: mutable.Buffer[ast.Statement] = ListBuffer(), -) extends FileContext(fileContext.projectContext, fileContext.packageName, fileContext.extractedExport, fileContext.neededImports) { +) extends FileContext(fileContext.projectContext, fileContext.packageName, fileContext.extractedExport, fileContext.neededImports, fileContext.localIdentifiers) { def addExtractedStatements(sts: List[ast.Statement]): Unit = extractedStatements.appendAll(sts) @@ -65,12 +76,28 @@ class ClassContext( case None => classOrInterface.exists(c => c.getName.asString != name) case scopeVal: Some[String] => classOrInterface.exists(c => c.getName.asString != scopeVal.get) }) + + def isClassName(name: SimpleName): Boolean = + classOrInterface.exists(c => c.getName == name) || parentClassContext.exists(c => c.isClassName(name)) + + def isInterface: Boolean = + classOrInterface.exists(c => { + c match + case c: ClassOrInterfaceDeclaration => c.isInterface + case _ => false + }) || parentClassContext.exists(c => c.isInterface) } class ParameterContext( val classContext: ClassContext, val parameters: mutable.Buffer[ast.Parameter] ) extends ClassContext(classContext.fileContext, classContext.classOrInterface, classContext.parentClassContext, classContext.extractedStatements) { + + override def isLocal(name: SimpleName): Boolean = + super.isLocal(name) + || + parameters.exists(p => p.name.escapedText == name.getIdentifier) + def isNonStaticMember(name: SimpleName): Boolean = classOrInterface.exists { _.getMembers.asScala.exists { @@ -87,7 +114,7 @@ class ParameterContext( def isStaticMember(name: SimpleName): Boolean = classOrInterface.exists { p => p match { - // we trat enum constants as static members + // we treat enum constants as static members case e: EnumDeclaration => e.getEntries.asScala.exists { entry => entry.getName == name } diff --git a/src/main/scala/java2typescript/transformer/expressions.scala b/src/main/scala/java2typescript/transformer/expressions.scala index f1f944c..5cd52cd 100644 --- a/src/main/scala/java2typescript/transformer/expressions.scala +++ b/src/main/scala/java2typescript/transformer/expressions.scala @@ -2,7 +2,7 @@ package java2typescript.transformer import com.github.javaparser.ast.NodeList import com.github.javaparser.ast.`type`.{ClassOrInterfaceType, Type} -import com.github.javaparser.ast.expr.{ArrayAccessExpr, ArrayCreationExpr, ArrayInitializerExpr, AssignExpr, BinaryExpr, CastExpr, ConditionalExpr, EnclosedExpr, Expression, FieldAccessExpr, InstanceOfExpr, LiteralExpr, MethodCallExpr, NameExpr, ObjectCreationExpr, SuperExpr, ThisExpr, UnaryExpr, VariableDeclarationExpr} +import com.github.javaparser.ast.expr.{ArrayAccessExpr, ArrayCreationExpr, ArrayInitializerExpr, AssignExpr, BinaryExpr, CastExpr, ConditionalExpr, EnclosedExpr, Expression, FieldAccessExpr, InstanceOfExpr, LiteralExpr, LongLiteralExpr, MethodCallExpr, NameExpr, ObjectCreationExpr, SuperExpr, ThisExpr, UnaryExpr, VariableDeclarationExpr} import java2typescript.analyseExports.Import import java2typescript.ast import java2typescript.ast.{ConditionalExpression, SyntaxKind} @@ -139,40 +139,6 @@ def transformUnaryExpression(context: ParameterContext, expr: UnaryExpr): ast.Pr transformExpression(context, expr.getExpression) ) -def transformObjectCreationExpression(context: ParameterContext, expr: ObjectCreationExpr): ast.Expression = - if (getTypeName(expr.getType).endsWith("Exception") || getTypeName(expr.getType).endsWith("Error")) - val name = expr.getType.getName - if (name.asString() == "Error") - return ast.NewExpression( - ast.Identifier("Error"), - transformArguments(context, expr.getArguments), - transformTypeArguments(context, expr.getTypeArguments) - ) - val args = expr.getArguments.asScala - if (args.length > 1) - println(s"WARN: ${expr.getType.getName}: all but the first error argument are dropped.") - - ast.NewExpression( - ast.Identifier("Error"), - List( - if (args.isEmpty) - ast.StringLiteral(name.toString) - else - ast.BinaryExpression( - ast.StringLiteral(s"$name: "), - transformExpression(context, args.head), - ast.PlusToken() - ) - ) - ) - else - context.addImportIfNeeded(getTypeScope(expr.getType), getTypeName(expr.getType)) - ast.NewExpression( - ast.Identifier(expr.getType.getName.getIdentifier), - transformArguments(context, expr.getArguments), - transformTypeArguments(context, expr.getTypeArguments) - ) - def transformArguments(context: ParameterContext, expressions: NodeList[Expression]) = expressions.asScala.map(transformExpression.curried(context)).toList diff --git a/src/main/scala/java2typescript/transformer/statements.scala b/src/main/scala/java2typescript/transformer/statements.scala index 9936f54..b14b195 100644 --- a/src/main/scala/java2typescript/transformer/statements.scala +++ b/src/main/scala/java2typescript/transformer/statements.scala @@ -51,7 +51,8 @@ def transformStatement(context: ParameterContext, stmt: Statement): ast.Statemen case stmt: TryStmt => transformTryStatement(context, stmt) case stmt: ExplicitConstructorInvocationStmt => ast.ExpressionStatement( ast.CallExpression( - ast.SuperKeyword() + ast.SuperKeyword(), + transformArguments(context, stmt.getArguments) ) ) case stmt: SwitchStmt => transformSwitchStatement(context, stmt) diff --git a/src/main/scala/java2typescript/transformer/transform.scala b/src/main/scala/java2typescript/transformer/transform.scala index 1c2fd0b..63dff50 100644 --- a/src/main/scala/java2typescript/transformer/transform.scala +++ b/src/main/scala/java2typescript/transformer/transform.scala @@ -38,16 +38,23 @@ def transformNameInContext(context: ParameterContext, name: SimpleName) = ast.Identifier(context.classOrInterface.get.getName.getIdentifier), transformName(name) ) - else + else if (context.isLocal(name) || context.isImportedName(name) || context.isClassName(name) || isBuiltInType(name)) transformName(name) + else + ast.PropertyAccessExpression( + ast.ThisKeyword(), + transformName(name) + ) def transformDeclaratorToVariable(context: ClassContext|ParameterContext, decl: VariableDeclarator): ast.VariableDeclaration = + val name = transformName(decl.getName) + context.addLocalName(name) val parameterContext = context match { case c: ParameterContext => c case c: ClassContext => ParameterContext(context, ListBuffer()) } ast.VariableDeclaration( - transformName(decl.getName), + name, transformType(context, decl.getType), decl.getInitializer.toScala.map(transformExpression.curried(parameterContext)) ) @@ -71,6 +78,9 @@ def transformModifier(modifier: Modifier): Option[ast.Modifier] = case Keyword.ABSTRACT => Some(ast.AbstractKeyword()) case Keyword.FINAL => None case Keyword.STRICTFP => None + case Keyword.VOLATILE => None + case Keyword.SYNCHRONIZED => None + case Keyword.TRANSIENT => None case key => throw new Error(s"Modifier $key not supported") def transformName(name: SimpleName): ast.Identifier = @@ -82,7 +92,15 @@ def transformType(context: FileContext, aType: Type): Option[ast.Type] = aType.getName.getIdentifier match case "Boolean" => Some(ast.BooleanKeyword()) case "String" => Some(ast.StringKeyword()) - case "Integer"|"Double" => Some(ast.NumberKeyword()) + case "Integer"|"Double"|"Long" => Some(ast.NumberKeyword()) + case "List"|"Collection" => + val types = transformTypeArguments(context, aType.getTypeArguments) + if (types.isEmpty) + Some(ast.ArrayType(ast.AnyKeyword())) + else if (types.length == 1) + Some(ast.ArrayType(types.head)) + else + throw new Error("Array type cannot have more then on type argument") case other => context.addImportIfNeeded(aType.getScope.toScala.map(f => f.getName.asString), aType.getName.asString) Some(ast.TypeReference(ast.Identifier(other), transformTypeArguments(context, aType.getTypeArguments))) @@ -100,8 +118,10 @@ def transformLiteral(expr: LiteralExpr): ast.Literal = expr match case expr: StringLiteralExpr => ast.StringLiteral(expr.getValue) - case expr: (IntegerLiteralExpr|DoubleLiteralExpr|LongLiteralExpr) => + case expr: (IntegerLiteralExpr|DoubleLiteralExpr) => ast.NumericLiteral(expr.getValue) + case expr: LongLiteralExpr => + ast.NumericLiteral(expr.getValue.stripSuffix("L")) case expr: BooleanLiteralExpr => if (expr.getValue) ast.TrueKeyword() diff --git a/src/main/scala/java2typescript/transformer/unsupported/transform.scala b/src/main/scala/java2typescript/transformer/unsupported/transform.scala index c8eead1..dfb9756 100644 --- a/src/main/scala/java2typescript/transformer/unsupported/transform.scala +++ b/src/main/scala/java2typescript/transformer/unsupported/transform.scala @@ -4,15 +4,13 @@ import java2typescript.ast import java2typescript.ast.SyntaxKind import com.github.javaparser.ast.CompilationUnit import com.github.javaparser.ast.body.{BodyDeclaration, ClassOrInterfaceDeclaration, ConstructorDeclaration, MethodDeclaration, TypeDeclaration} -import java2typescript.transformer.{FileContext, ProjectContext, createConstructorOverloads, createMethodOverloads, groupMethodsByName, transformHeritage, transformModifier, transformName, transformParameter, transformType} +import java2typescript.transformer.{FileContext, ProjectContext, createConstructorOverloads, createMethodOverloads, groupMethodsByName, isDroppableInterface, transformHeritage, transformModifier, transformName, transformParameter, transformType} import scala.jdk.CollectionConverters.* import scala.jdk.OptionConverters.* -val unsupportedKeywords = List( - "synchronized", - "volatile", - "transient" +val unsupportedWords = List( + "SoftReference" ) def checkUnsupported(code: String) = @@ -21,10 +19,9 @@ def checkUnsupported(code: String) = .replaceAll("\\\\\"", "") // remove escaped quotes .replaceAll("\"[^\"]*\"", "\"\"") // remove everything between quotes .replaceAll("//.*", "") // remove single line comments - .toLowerCase() - unsupportedKeywords.exists { - keyword => reduced.contains(keyword) + unsupportedWords.exists { + word => reduced.contains(word) } def transformCompilationUnit(context: ProjectContext, cu: CompilationUnit): List[ast.Node] = @@ -136,6 +133,6 @@ def transformClassOrInterfaceDeclaration( transformName(decl.getName), members = constructorsWithOverloads ::: methodsWithOverloads, modifiers = modifiersVal, - heritageClauses = transformHeritage(context, decl.getExtendedTypes, SyntaxKind.ExtendsKeyword).toList - ::: transformHeritage(context, decl.getImplementedTypes, SyntaxKind.ImplementsKeyword).toList + heritageClauses = transformHeritage(context, decl.getExtendedTypes.asScala.toList, SyntaxKind.ExtendsKeyword).toList + ::: transformHeritage(context, decl.getImplementedTypes.asScala.filter(t => !isDroppableInterface(t.getName)).toList, SyntaxKind.ImplementsKeyword).toList ) diff --git a/src/test/resources/fixtures/anonymous-classes.md b/src/test/resources/fixtures/anonymous-classes.md new file mode 100644 index 0000000..106d1f1 --- /dev/null +++ b/src/test/resources/fixtures/anonymous-classes.md @@ -0,0 +1,37 @@ +# Anonymous Classes +## simple anonymous class that implements interface +options: debug +```java +interface Filter { + boolean filter(String x); +} +``` +```java +import Filter; +class FilterFactory { + public Filter getFilter() { + return new Filter() { + public boolean filter(String x) { + return x == "value"; + } + }; + } +} +``` +```typescript +export abstract class Filter { + public abstract filter(x: string): boolean; +} +``` +```typescript +import { Filter } from "./Filter.ts"; +export class FilterFactory { + public getFilter(): Filter { + return new (class extends Filter { + public filter(x: string) { + return x === "value"; + } + })(); + } +} +``` diff --git a/src/test/resources/fixtures/array.md b/src/test/resources/fixtures/array.md index d528e32..a7c860f 100644 --- a/src/test/resources/fixtures/array.md +++ b/src/test/resources/fixtures/array.md @@ -6,7 +6,7 @@ Coordinate[] coordinates = new Coordinate[] { centerPt.copy(), radiusPt.copy() } Coordinate[] coords = { new Coordinate(), new Coordinate() }; ``` ```typescript -let coordinates: Coordinate[] = [centerPt.copy(), radiusPt.copy()]; +let coordinates: Coordinate[] = [this.centerPt.copy(), this.radiusPt.copy()]; let coords: Coordinate[] = [new Coordinate(), new Coordinate()]; ``` @@ -16,7 +16,7 @@ options: methodBody Integer num = arr[0]; ``` ```typescript -let num: number = arr[0]; +let num: number = this.arr[0]; ``` ## Array brackets position diff --git a/src/test/resources/fixtures/assertions.md b/src/test/resources/fixtures/assertions.md index 028e14d..2d1a2f9 100644 --- a/src/test/resources/fixtures/assertions.md +++ b/src/test/resources/fixtures/assertions.md @@ -15,6 +15,6 @@ options: methodBody assert x == 132; ``` ```typescript -if (!(x === 132)) +if (!(this.x === 132)) throw new Error("Assertion failed"); ``` diff --git a/src/test/resources/fixtures/assignments.md b/src/test/resources/fixtures/assignments.md index 1ed02f6..6493fd9 100644 --- a/src/test/resources/fixtures/assignments.md +++ b/src/test/resources/fixtures/assignments.md @@ -1,4 +1,4 @@ -# Declarations +# Assignments ## string variable options: methodBody @@ -39,6 +39,7 @@ let c: CustomType = new CustomType(123, "abc"); ## increment, decrement options: methodBody ```java +int i = 5; i++; i--; ++i; @@ -47,6 +48,7 @@ i += 1; i -= 2; ``` ```typescript +let i: number = 5; i++; i--; ++i; diff --git a/src/test/resources/fixtures/calls.md b/src/test/resources/fixtures/calls.md index 7ed86dc..d18869f 100644 --- a/src/test/resources/fixtures/calls.md +++ b/src/test/resources/fixtures/calls.md @@ -2,9 +2,11 @@ ## Method Call options: methodBody ```java +Factory factory = new Factory(); factory.createSomething(); ``` ```typescript +let factory: Factory = new Factory(); factory.createSomething(); ``` ## Function Call @@ -13,5 +15,5 @@ options: methodBody createSomething(); ``` ```typescript -createSomething(); +this.createSomething(); ``` diff --git a/src/test/resources/fixtures/classes.md b/src/test/resources/fixtures/classes.md index 7c03962..7898821 100644 --- a/src/test/resources/fixtures/classes.md +++ b/src/test/resources/fixtures/classes.md @@ -11,10 +11,23 @@ export class A { ## Basic interface ```java -interface A {} +interface A { +} +``` +```typescript +export abstract class A { +} +``` + +## Interface with method +```java +interface A { + void iMethod(); +} ``` ```typescript export abstract class A { + public abstract iMethod(): void; } ``` @@ -207,10 +220,12 @@ export class A implements B, C { ## static initializer ```java +import System; + class GeometryOverlay { static String OVERLAY_PROPERTY_NAME = "test"; static { - setOverlayImpl(System.getProperty(OVERLAY_PROPERTY_NAME)); + setOverlayImpl(OVERLAY_PROPERTY_NAME); } static void setOverlayImpl(String overlayImplCode) { } @@ -222,7 +237,7 @@ export class GeometryOverlay { static setOverlayImpl(overlayImplCode: string): void { } } -GeometryOverlay.setOverlayImpl(System.getProperty(GeometryOverlay.OVERLAY_PROPERTY_NAME)); +GeometryOverlay.setOverlayImpl(GeometryOverlay.OVERLAY_PROPERTY_NAME); ``` ## abstract class @@ -282,3 +297,39 @@ export class C { private ab: B = new B(); } ``` + +## Super Constructor +```java +class A { + private String val; + public A(String x) { + val = x; + } +} +``` +```java +import A; + +class B extends A { + public B(String y) { + super(y); + } +} +``` +```typescript +export class A { + private val: string; + public constructor(x: string) { + this.val = x; + } +} +``` + +```typescript +import { A } from "./A.ts"; +export class B extends A { + public constructor(y: string) { + super(y); + } +} +``` diff --git a/src/test/resources/fixtures/control-structures.md b/src/test/resources/fixtures/control-structures.md index e41462b..2767b40 100644 --- a/src/test/resources/fixtures/control-structures.md +++ b/src/test/resources/fixtures/control-structures.md @@ -62,16 +62,16 @@ while (num < 4) { } ``` ```typescript -while (num < 4) { - num = func(); +while (this.num < 4) { + this.num = this.func(); } ``` ## for options: methodBody ```java -for (int i = start; ; i += inc) { - if (i >= max) { +for (int i = 1; ; i += 2) { + if (i >= 10) { break; } else { continue; @@ -79,8 +79,8 @@ for (int i = start; ; i += inc) { } ``` ```typescript -for (let i: number = start;; i += inc) { - if (i >= max) { +for (let i: number = 1;; i += 2) { + if (i >= 10) { break; } else { @@ -92,12 +92,14 @@ for (let i: number = start;; i += inc) { ## for2 options: methodBody ```java -for (j = 0; j < last; j++) +int i = 2; +for (int j = 0; j < last; j++) newCoordinates[j] = coordinates[(i + j) % last]; ``` ```typescript -for (j = 0; j < last; j++) - newCoordinates[j] = coordinates[(i + j) % last]; +let i: number = 2; +for (let j: number = 0; j < this.last; j++) + this.newCoordinates[j] = this.coordinates[(i + j) % this.last]; ``` ## switch @@ -116,15 +118,15 @@ switch(num) { } ``` ```typescript -switch (num) { +switch (this.num) { case 1: - func(); + this.func(); break; case 2: case 3: - func2(); + this.func2(); break; - default: func3(); + default: this.func3(); } ``` @@ -136,19 +138,21 @@ for (Geometry hole : holesFixed) { } ``` ```typescript -for (let hole: Geometry of holesFixed) { - holes.add(hole); +for (let hole: Geometry of this.holesFixed) { + this.holes.add(hole); } ``` ## do while options: methodBody ```java +int i = 1; do { i++; } while (i < 10); ``` ```typescript +let i: number = 1; do { i++; } while (i < 10); diff --git a/src/test/resources/fixtures/data-types.md b/src/test/resources/fixtures/data-types.md new file mode 100644 index 0000000..343dacd --- /dev/null +++ b/src/test/resources/fixtures/data-types.md @@ -0,0 +1,31 @@ +# Data Types +## Long +options: methodBody +```java +long serialVersionUID = 4902022702746614570L; +``` +```typescript +let serialVersionUID: number = 4902022702746614570; +``` + +## List +options: methodBody +```java +List a = new ArrayList(); +List b = new ArrayList(); +``` +```typescript +let a: T[] = []; +let b: any[] = []; +``` + +## Collection +options: methodBody +```java +Collection a = new Collection(); +Collection b = new Collection(); +``` +```typescript +let a: T[] = []; +let b: any[] = []; +``` diff --git a/src/test/resources/fixtures/enum.md b/src/test/resources/fixtures/enum.md index f547cae..f89b7d6 100644 --- a/src/test/resources/fixtures/enum.md +++ b/src/test/resources/fixtures/enum.md @@ -16,7 +16,19 @@ export enum Test { ``` ## Enum with static fields and methods +```json +{ + "customImports": [ + { + "javaName": "EnumSet", + "fixedPath": "customLocation/EnumSet.ts" + } + ] +} +``` ```java +import java.util.EnumSet; + enum Test { X, Y; @@ -25,6 +37,7 @@ enum Test { } ``` ```typescript +import { EnumSet } from "customLocation/EnumSet.ts"; export enum Test { X, Y diff --git a/src/test/resources/fixtures/expressions.md b/src/test/resources/fixtures/expressions.md index e4ef396..83a3e2d 100644 --- a/src/test/resources/fixtures/expressions.md +++ b/src/test/resources/fixtures/expressions.md @@ -78,18 +78,18 @@ options: methodBody String text = (String) someVar; ``` ```typescript -let text: string = someVar as string; +let text: string = this.someVar as string; ``` ## conditional expression options: methodBody ```java -Integer val = condition - ? var1 - : var2; +Integer val = someVar == 3 + ? 4 + : 5; ``` ```typescript -let val: number = condition ? var1 : var2; +let val: number = this.someVar === 3 ? 4 : 5; ``` ## instanceof @@ -98,5 +98,5 @@ options: methodBody Boolean x = a instanceof B; ``` ```typescript -let x: boolean = a instanceof B; +let x: boolean = this.a instanceof B; ``` diff --git a/src/test/resources/fixtures/jts-classes/indexed-point-in-area-locator.md b/src/test/resources/fixtures/jts-classes/indexed-point-in-area-locator.md index 8c5b88b..03f2c1a 100644 --- a/src/test/resources/fixtures/jts-classes/indexed-point-in-area-locator.md +++ b/src/test/resources/fixtures/jts-classes/indexed-point-in-area-locator.md @@ -1,5 +1,15 @@ # IndexedPointInAreaLocator ## whole class +```json +{ + "customImports": [ + { + "packageName": "org.locationtech.jts.geom.util", + "javaName": "LinearComponentExtracter" + } + ] +} +``` ```java /* * Copyright (c) 2016 Vivid Solutions. @@ -185,87 +195,88 @@ public class IndexedPointInAreaLocator } ``` ```typescript +import { LinearComponentExtracter } from "../../geom/util/LinearComponentExtracter.ts"; export class IndexedPointInAreaLocator implements PointOnGeometryLocator { - private geom: Geometry; - private index: IntervalIndexedGeometry = null; - public constructor(g: Geometry) { - this.geom = g; - } - public locate(p: Coordinate): number { - if (this.index === null) - this.createIndex(); - let rcc: RayCrossingCounter = new RayCrossingCounter(p); - let visitor: SegmentVisitor = new SegmentVisitor(rcc); - this.index.query(p.y, p.y, visitor); - return rcc.getLocation(); - } - private createIndex(): void { - if (this.index === null) { - this.index = new IntervalIndexedGeometry(this.geom); - this.geom = null; + private geom: Geometry; + private index: IntervalIndexedGeometry = null; + public constructor(g: Geometry) { + this.geom = g; + } + public locate(p: Coordinate): number { + if (this.index === null) + this.createIndex(); + let rcc: RayCrossingCounter = new RayCrossingCounter(p); + let visitor: SegmentVisitor = new SegmentVisitor(rcc); + this.index.query(p.y, p.y, visitor); + return rcc.getLocation(); + } + private createIndex(): void { + if (this.index === null) { + this.index = new IntervalIndexedGeometry(this.geom); + this.geom = null; + } } - } } export class SegmentVisitor implements ItemVisitor { - private counter: RayCrossingCounter; - public constructor(counter: RayCrossingCounter) { - this.counter = counter; - } - public visitItem(item: Object): void { - let seg: LineSegment = item as LineSegment; - this.counter.countSegment(seg.getCoordinate(0), seg.getCoordinate(1)); - } + private counter: RayCrossingCounter; + public constructor(counter: RayCrossingCounter) { + this.counter = counter; + } + public visitItem(item: Object): void { + let seg: LineSegment = item as LineSegment; + this.counter.countSegment(seg.getCoordinate(0), seg.getCoordinate(1)); + } } export class IntervalIndexedGeometry { - private isEmpty: boolean; - private index: SortedPackedIntervalRTree = new SortedPackedIntervalRTree(); - public constructor(geom: Geometry) { - if (geom.isEmpty()) - this.isEmpty = true; - else { - this.isEmpty = false; - this.init(geom); + private isEmpty: boolean; + private index: SortedPackedIntervalRTree = new SortedPackedIntervalRTree(); + public constructor(geom: Geometry) { + if (geom.isEmpty()) + this.isEmpty = true; + else { + this.isEmpty = false; + this.init(geom); + } } - } - private init(geom: Geometry): void { - let lines: List = LinearComponentExtracter.getLines(geom); - for (let i: Iterator = lines.iterator(); i.hasNext();) { - let line: LineString = i.next() as LineString; - if (!line.isClosed()) - continue; - let pts: Coordinate[] = line.getCoordinates(); - this.addLine(pts); + private init(geom: Geometry): void { + let lines: any[] = LinearComponentExtracter.getLines(geom); + for (let i: Iterator = lines.iterator(); i.hasNext();) { + let line: LineString = i.next() as LineString; + if (!line.isClosed()) + continue; + let pts: Coordinate[] = line.getCoordinates(); + this.addLine(pts); + } } - } - private addLine(pts: Coordinate[]): void { - for (let i: number = 1; i < pts.length; i++) { - let seg: LineSegment = new LineSegment(pts[i - 1], pts[i]); - let min: number = Math.min(seg.p0.y, seg.p1.y); - let max: number = Math.max(seg.p0.y, seg.p1.y); - this.index.insert(min, max, seg); + private addLine(pts: Coordinate[]): void { + for (let i: number = 1; i < pts.length; i++) { + let seg: LineSegment = new LineSegment(pts[i - 1], pts[i]); + let min: number = Math.min(seg.p0.y, seg.p1.y); + let max: number = Math.max(seg.p0.y, seg.p1.y); + this.index.insert(min, max, seg); + } } - } - public query(min: number, max: number): List; - public query(min: number, max: number, visitor: ItemVisitor): void; - public query(...args: any[]): List { - if (args.length === 2 && typeof args[0] === "number" && typeof args[1] === "number") { - let min: number = args[0]; - let max: number = args[1]; - if (this.isEmpty) - return new ArrayList(); - let visitor: ArrayListVisitor = new ArrayListVisitor(); - this.index.query(min, max, visitor); - return visitor.getItems(); + public query(min: number, max: number): any[]; + public query(min: number, max: number, visitor: ItemVisitor): void; + public query(...args: any[]): any[] { + if (args.length === 2 && typeof args[0] === "number" && typeof args[1] === "number") { + let min: number = args[0]; + let max: number = args[1]; + if (this.isEmpty) + return []; + let visitor: ArrayListVisitor = new ArrayListVisitor(); + this.index.query(min, max, visitor); + return visitor.getItems(); + } + if (args.length === 3 && typeof args[0] === "number" && typeof args[1] === "number" && args[2] instanceof ItemVisitor) { + let min: number = args[0]; + let max: number = args[1]; + let visitor: ItemVisitor = args[2]; + if (this.isEmpty) + return; + this.index.query(min, max, visitor); + } + throw new Error("overload does not exist"); } - if (args.length === 3 && typeof args[0] === "number" && typeof args[1] === "number" && args[2] instanceof ItemVisitor) { - let min: number = args[0]; - let max: number = args[1]; - let visitor: ItemVisitor = args[2]; - if (this.isEmpty) - return; - this.index.query(min, max, visitor); - } - throw new Error("overload does not exist"); - } } ``` diff --git a/src/test/resources/fixtures/properties-parameters.md b/src/test/resources/fixtures/properties-parameters.md index 9e5a512..48d5b86 100644 --- a/src/test/resources/fixtures/properties-parameters.md +++ b/src/test/resources/fixtures/properties-parameters.md @@ -21,3 +21,35 @@ export class A { } } ``` + +## Using local vs non local names +```java +class A extends X { + private int a; + protected int b() { + return 2; + } + public static int c() { + return 3; + } + public void method(int d) { + int e = 4; + nonLocal(a, b(), c(), d, e); + } +} +``` +```typescript +export class A extends X { + private a: number; + protected b(): number { + return 2; + } + public static c(): number { + return 3; + } + public method(d: number): void { + let e: number = 4; + this.nonLocal(this.a, this.b(), A.c(), d, e); + } +} +``` diff --git a/src/test/resources/fixtures/throw-try-catch-finally.md b/src/test/resources/fixtures/throw-try-catch-finally.md index e346f9b..f8a7701 100644 --- a/src/test/resources/fixtures/throw-try-catch-finally.md +++ b/src/test/resources/fixtures/throw-try-catch-finally.md @@ -54,12 +54,12 @@ finally { ``` ```typescript try { - something(); + this.something(); } catch (e: any) { - somethingOnError(); + this.somethingOnError(); } finally { - somethingFinally(); + this.somethingFinally(); } ```