Skip to content

Commit c513d9a

Browse files
committed
JavaDoc parsing changes and switch to fully qualified names
1 parent 69f5060 commit c513d9a

File tree

8 files changed

+170
-105
lines changed

8 files changed

+170
-105
lines changed

build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ repositories {
1313
dependencies {
1414
testImplementation(kotlin("test"))
1515

16-
implementation("com.github.javaparser:javaparser-core:3.26.4")
1716
implementation("com.github.javaparser:javaparser-symbol-solver-core:3.26.4")
1817
}
1918

src/main/kotlin/Main.kt

Lines changed: 16 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
package main.kotlin
2+
23
import generator.LuaEmitter
34
import parser.ClassParser
45
import parser.JavaSourceParser
@@ -27,36 +28,23 @@ fun main(args: Array<String>) {
2728
// Initialize the emitter
2829
val luaEmitter = LuaEmitter()
2930

30-
args.filterNot { it.startsWith("--output-dir=") }.forEach { jarFilePath ->
31-
val file = File(jarFilePath)
32-
if (!file.exists()) {
33-
println("Warning: File $jarFilePath does not exist.")
34-
return@forEach
35-
}
31+
var parser: ClassParser = JavaSourceParser(args.filter { !it.startsWith("--output-dir=") }.map { File(it) })
3632

37-
val parser: ClassParser = if (jarFilePath.endsWith("-sources.jar")) {
38-
println("Using JavaSourceParser for $jarFilePath")
39-
JavaSourceParser()
40-
} else {
41-
println("Using CompiledClassParser for $jarFilePath")
42-
JavaSourceParser() // Placeholder for compiled class parser
43-
}
4433

45-
try {
46-
val parsedClasses = parser.parse(JarFile(file))
47-
48-
parsedClasses.forEach { parsedClass ->
49-
val luaOutput = luaEmitter.emit(parsedClass)
50-
val outputFile = File(outputDir, "${parsedClass.name}.lua")
51-
outputFile.writeText(luaOutput)
52-
println("Generated Lua stubs for ${parsedClass.name} in ${outputFile.absolutePath}")
53-
}
54-
val importsOutput = luaEmitter.emitAvailableImports(parsedClasses)
55-
val importsFile = File(outputDir, "imports.lua")
56-
importsFile.writeText(importsOutput)
57-
println("Generated Lua imports in ${importsFile.absolutePath}")
58-
} catch (e: Exception) {
59-
println("Error processing $jarFilePath: ${e.message}")
34+
try {
35+
val parsedClasses = parser.parse()
36+
37+
parsedClasses.forEach { parsedClass ->
38+
val luaOutput = luaEmitter.emit(parsedClass)
39+
val outputFile = File(outputDir, "${parsedClass.packageName}.${parsedClass.name}.lua")
40+
outputFile.writeText(luaOutput)
41+
println("Generated Lua stubs for ${parsedClass.name} in ${outputFile.absolutePath}")
6042
}
43+
val importsOutput = luaEmitter.emitAvailableImports(parsedClasses)
44+
val importsFile = File(outputDir, "imports.lua")
45+
importsFile.writeText(importsOutput)
46+
println("Generated Lua imports in ${importsFile.absolutePath}")
47+
} catch (e: Exception) {
48+
println("Error processing: ${e.message}")
6149
}
6250
}

src/main/kotlin/generator/LuaEmitter.kt

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package generator
22

33
import parser.ParsedClass
44
import parser.Visibility
5-
import util.extractJavaDocInfo
65
import util.mapJavaTypeToLua
76

87
class LuaEmitter {
@@ -11,7 +10,7 @@ class LuaEmitter {
1110
sb.appendLine("---@meta")
1211
sb.appendLine("-- ${parsedClass.packageName}.${parsedClass.name}")
1312
sb.appendLine(
14-
"---@class ${parsedClass.name}${
13+
"---@class ${parsedClass.packageName}.${parsedClass.name}${
1514
if (parsedClass.extendedTypes.isNotEmpty() || parsedClass.implementedTypes.isNotEmpty())
1615
": " + (parsedClass.extendedTypes + parsedClass.implementedTypes).joinToString(", ")
1716
else ""
@@ -28,7 +27,6 @@ class LuaEmitter {
2827
}
2928

3029
parsedClass.constructors.forEach { constructor ->
31-
val doc = extractJavaDocInfo(constructor.comment)
3230

3331
val params = constructor.parameters.joinToString(", ") { param ->
3432
val paramName = param.name
@@ -37,21 +35,19 @@ class LuaEmitter {
3735
}
3836

3937
val returnType = mapJavaTypeToLua(constructor.returnType)
40-
sb.appendLine("---@overload fun($params): $returnType ${doc.returnComment ?: ""}")
38+
sb.appendLine("---@overload fun($params): $returnType")
4139
}
4240

4341
sb.appendLine("local ${parsedClass.name} = {}\n")
4442

45-
val docInfo = extractJavaDocInfo(parsedClass.classComment)
46-
if (docInfo.mainComment.isNotBlank()) {
47-
sb.insert(0, "--- ${docInfo.mainComment}\n")
43+
val docInfo = parsedClass.classComment
44+
if (docInfo!!.isNotBlank()) {
45+
sb.insert(0, "--- ${docInfo}\n")
4846
}
4947

5048

5149
// Process methods
5250
parsedClass.methods.forEach { method ->
53-
val doc = extractJavaDocInfo(method.comment)
54-
5551
if (method.isDeprecated) {
5652
sb.appendLine("---@deprecated")
5753
}
@@ -70,10 +66,10 @@ class LuaEmitter {
7066
Visibility.PRIVATE -> sb.appendLine("---@private")
7167
}
7268

73-
sb.appendLine("---@return ${mapJavaTypeToLua(method.returnType)} ${doc.returnComment ?: ""}")
69+
sb.appendLine("---@return ${mapJavaTypeToLua(method.returnType)} ${method.returnComment ?: ""}")
7470

75-
if (doc.mainComment.isNotBlank()) {
76-
sb.appendLine("--- ${doc.mainComment}")
71+
if (method.comment!!.isNotBlank()) {
72+
sb.appendLine("--- ${method.comment}")
7773
}
7874

7975

src/main/kotlin/parser/ClassParser.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ package parser
33
import java.util.jar.JarFile
44

55
interface ClassParser {
6-
fun parse(jar: JarFile): List<ParsedClass>
6+
fun parse(): List<ParsedClass>
77
}

src/main/kotlin/parser/JavaSourceParser.kt

Lines changed: 106 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,92 @@
11
package parser
22

33
import com.github.javaparser.JavaParser
4+
import com.github.javaparser.ParserConfiguration
5+
import com.github.javaparser.ast.CompilationUnit
46
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration
57
import com.github.javaparser.ast.body.ConstructorDeclaration
68
import com.github.javaparser.ast.body.EnumDeclaration
79
import com.github.javaparser.ast.body.FieldDeclaration
810
import com.github.javaparser.ast.body.MethodDeclaration
9-
import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration
11+
import com.github.javaparser.ast.type.Type
12+
import com.github.javaparser.resolution.SymbolResolver
13+
import com.github.javaparser.resolution.UnsolvedSymbolException
14+
import com.github.javaparser.symbolsolver.JavaSymbolSolver
15+
import com.github.javaparser.symbolsolver.javaparsermodel.JavaParserFacade
1016
import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver
11-
import com.github.javaparser.utils.SourceRoot
12-
import util.extractJavaDocInfo
17+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JarTypeSolver
18+
import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver
19+
import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver
20+
import com.github.javaparser.symbolsolver.resolution.typesolvers.TypeSolverBuilder
21+
import util.extractMainComment
22+
import util.extractParamComment
23+
import util.extractReturnComment
24+
import util.mapJavaTypeToLua
25+
import java.io.File
1326
import java.io.InputStream
27+
import java.nio.file.Files
1428
import java.util.jar.JarEntry
1529
import java.util.jar.JarFile
30+
import kotlin.jvm.optionals.getOrNull
31+
32+
class JavaSourceParser(private val jarFiles: List<File>) : ClassParser {
33+
private val solver = CombinedTypeSolver()
34+
private val parserConfiguration = ParserConfiguration()
35+
private val parser: JavaParser = JavaParser(parserConfiguration)
36+
private val javaParserFacade: JavaParserFacade = JavaParserFacade.get(solver)
37+
val parsedClasses = mutableMapOf<String, ParsedClass>() // Map to store parsed classes by their fully qualified name
38+
39+
init {
40+
// Add the reflection type solver to the combined type solver
41+
solver.add(ReflectionTypeSolver())
42+
43+
// Process each source JAR
44+
// I don't like this at all however it seems to be the only way to let it parse from source JARS
45+
// I suppose we could use a custom type solver
46+
// Or move away from source jars
47+
jarFiles.forEach { jar ->
48+
val tempDir = Files.createTempDirectory("source_jar")
49+
JarFile(jar).use { jarFile ->
50+
jarFile.entries().asSequence()
51+
.filter { it.name.endsWith(".java") }
52+
.forEach { entry ->
53+
val outputFile = tempDir.resolve(entry.name)
54+
Files.createDirectories(outputFile.parent)
55+
jarFile.getInputStream(entry).use { input ->
56+
Files.copy(input, outputFile)
57+
}
58+
}
59+
}
60+
solver.add(JavaParserTypeSolver(tempDir.toFile()))
61+
}
62+
63+
// Set the symbol resolver in the parser configuration
64+
val symbolResolver: SymbolResolver = JavaSymbolSolver(solver)
65+
parserConfiguration.setSymbolResolver(symbolResolver)
66+
}
1667

17-
class JavaSourceParser : ClassParser {
18-
private val parser = JavaParser()
68+
override fun parse(): List<ParsedClass> {
1969

20-
override fun parse(jar: JarFile): List<ParsedClass> {
21-
val parsedClasses = mutableListOf<ParsedClass>()
70+
val jars = jarFiles.map { JarFile(it) }
2271

23-
jar.entries().asSequence().filter { it.name.endsWith(".java") }.forEach { entry ->
24-
parseEntry(jar, entry)?.let { parsedClasses.add(it) }
72+
jars.forEach { jar ->
73+
jar.entries().asSequence().filter { it.name.endsWith(".java") }.forEach { entry ->
74+
parseJarEntry(jar, entry)
75+
}
2576
}
2677

27-
return parsedClasses
78+
return parsedClasses.values.toList()
2879
}
2980

30-
private fun parseEntry(jarFile: JarFile, entry: JarEntry): ParsedClass? {
81+
private fun parseJarEntry(jarFile: JarFile, entry: JarEntry): ParsedClass? {
3182
val inputStream: InputStream = jarFile.getInputStream(entry)
3283
val cu = parser.parse(inputStream).result.orElse(null) ?: return null
3384
inputStream.close()
85+
return parseEntry(cu)
86+
}
3487

88+
private fun parseEntry(cu: CompilationUnit): ParsedClass? {
3589
val pkgName = cu.packageDeclaration.map { it.nameAsString }.orElse("")
36-
3790
var parsedClass: ParsedClass? = null
3891

3992
// Handle enum
@@ -46,13 +99,14 @@ class JavaSourceParser : ClassParser {
4699
name = it.nameAsString,
47100
type = enumDecl.nameAsString,
48101
visibility = Visibility.PUBLIC,
49-
comment = it.comment.map { c -> extractJavaDocInfo(c.content).mainComment }.orElse(null)
102+
comment = extractMainComment(it.javadoc.getOrNull())
50103
)
51104
},
52105
methods = emptyList(),
53-
classComment = enumDecl.comment.map { extractJavaDocInfo(it.content).mainComment }.orElse(null),
106+
classComment = extractMainComment(enumDecl.javadoc.getOrNull()),
54107
isEnum = true
55108
)
109+
parsedClasses[enumDecl.fullyQualifiedName.toString()] = parsedClass!!
56110
}
57111

58112
// Handle class or interface
@@ -67,12 +121,13 @@ class JavaSourceParser : ClassParser {
67121
packageName = pkgName,
68122
fields = fields,
69123
methods = methods, // Combine methods
70-
classComment = cls.comment.map { extractJavaDocInfo(it.content).mainComment }.orElse(null),
71-
extendedTypes = cls.extendedTypes.map { it.asString() },
72-
implementedTypes = cls.implementedTypes.map { it.asString() },
124+
classComment = extractMainComment(cls.javadoc.getOrNull()),
125+
extendedTypes = cls.extendedTypes.map { getFullyQualifiedName(it) },
126+
implementedTypes = cls.implementedTypes.map { getFullyQualifiedName(it) },
73127
constructors = cls.constructors.map { parseConstructor(it, cls.nameAsString) },
74128
isEnum = false
75129
)
130+
parsedClasses[cls.fullyQualifiedName.toString()] = parsedClass!!
76131
}
77132

78133
return parsedClass
@@ -93,7 +148,7 @@ class JavaSourceParser : ClassParser {
93148
)
94149
},
95150
visibility = Visibility.fromString(constructor.accessSpecifier.toString().uppercase()),
96-
comment = constructor.comment.map { extractJavaDocInfo(it.content).mainComment }.orElse(null),
151+
comment = extractMainComment(constructor.javadoc.getOrNull()),
97152
isDeprecated = constructor.annotations.any { it.nameAsString == "Deprecated" },
98153
isAsync = false, // Constructors are not async
99154
isConstructor = true
@@ -103,19 +158,21 @@ class JavaSourceParser : ClassParser {
103158
private fun parseMethod(method: MethodDeclaration, className: String): ParsedMethod {
104159
return ParsedMethod(
105160
name = method.nameAsString,
106-
returnType = method.type.toString(),
161+
returnType = getFullyQualifiedName(method.type),
107162
parameters = method.parameters.map { param ->
108163
// Check for nullability in the parameter type. Not sure why but it seems to include annotations in the type string
109164
val isNullableComment = param.type.toString()?.contains("@Nullable") ?: false
110165

111166
ParsedParameter(
112167
name = param.nameAsString,
113-
type = param.type.toString().replace("@Nullable ", ""),
168+
type = getFullyQualifiedName(param.type),
114169
required = !isNullableComment,
170+
comment = extractParamComment(method.javadoc.getOrNull(), param.nameAsString)
115171
)
116172
},
117173
visibility = Visibility.fromString(method.accessSpecifier.toString().uppercase()),
118-
comment = method.comment.map { extractJavaDocInfo(it.content).mainComment }.orElse(null),
174+
comment = method.javadoc.getOrNull()?.description?.toText()?.replace("\n", " ") ?: "",
175+
returnComment = extractReturnComment(method.javadoc.getOrNull()),
119176
isDeprecated = method.annotations.any { it.nameAsString == "Deprecated" },
120177
isAsync = method.nameAsString.contains(
121178
"Async",
@@ -128,10 +185,36 @@ class JavaSourceParser : ClassParser {
128185
return field.variables.map { variable ->
129186
ParsedField(
130187
name = variable.nameAsString,
131-
type = field.elementType.asString(),
188+
type = getFullyQualifiedName(field.elementType),
132189
visibility = Visibility.fromString(field.accessSpecifier.toString().uppercase()),
133-
comment = field.comment.map { extractJavaDocInfo(it.content).mainComment }.orElse(null)
190+
comment = extractMainComment(field.javadoc.getOrNull())
134191
)
135192
}
136193
}
194+
195+
// Slightly based off of : https://github.com/freggy/blua/blob/2408c0e0fdbc29c2fd38ed9083ab437d31eb83e2/javadump/src/main/kotlin/dev/freggy/blua/javadump/Main.kt#L136-L167
196+
fun getFullyQualifiedName(
197+
type: Type,
198+
): String {
199+
return try {
200+
val resolvedType = javaParserFacade.convertToUsage(type)
201+
if (resolvedType.isReferenceType) {
202+
mapJavaTypeToLua(resolvedType.asReferenceType().qualifiedName)
203+
} else {
204+
mapJavaTypeToLua(type.asString())
205+
}
206+
} catch (e: Exception) {
207+
when (e) {
208+
is UnsupportedOperationException -> {
209+
mapJavaTypeToLua(type.asString())
210+
}
211+
212+
is UnsolvedSymbolException -> {
213+
"any" // Fallback to "any" for unresolved types
214+
}
215+
216+
else -> throw e // Rethrow unexpected exceptions
217+
}
218+
}
219+
}
137220
}

src/main/kotlin/parser/ParsedMethod.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ data class ParsedMethod(
66
val visibility: Visibility,
77
val parameters: List<ParsedParameter>,
88
val comment: String?,
9+
val returnComment: String? = null,
910
val isDeprecated: Boolean = false,
1011
val isAsync: Boolean = false,
1112
val isConstructor: Boolean = false,

0 commit comments

Comments
 (0)