-
-
Notifications
You must be signed in to change notification settings - Fork 23
Description
I have been going through a library and was creating typings for it's various different .aar files. I noticed some issues that have already been highlighted here: #83 (@NathanWalker) and #75 (@farfromrefug).
Since I had a build issue with the generated typings I set about investigating the issues. I am proficient in TypeScript, however I'm no expert in terms of Kotlin which meant I could see what the issues were in the typing files, but wasn't sure how to solve them. Therefore, I investigated the issues via Claude Code and the solutions that were generated have solved my issues. The following description was also generated by Claude Code, I did so to make sure that everything that was found is listed accurately:
Summary
While generating TypeScript definitions for Kotlin-based Android libraries (specifically MiSnap SDK v5.8.1), three critical issues were discovered that prevent proper type generation. This document outlines each issue, provides examples, and proposes fixes.
Environment
- DTS Generator Version: 4.0.0
- Test Library: MiSnap SDK v5.8.1 (Kotlin-based Android library)
- Kotlin Version: 1.8.20 (as seen in generated code)
- Java Target: 17
Issue 1: Kotlin Serializer Classes Generate Invalid TypeScript
Problem
Kotlin's
kotlinx.serializationcompiler generates classes with double dollar signs ($$serializer) in their bytecode names. The DTS generator processes these, creating invalid TypeScript with empty module declarations.Example from Bytecode
com/miteksystems/misnap/core/Barcode$$serializer.class com/miteksystems/misnap/core/MiSnapSettings$$serializer.classGenerated TypeScript (Invalid)
export module { // ❌ Empty module name! export module Barcode { export class serializer extends kotlinx.serialization.internal.GeneratedSerializer<com.miteksystems.misnap.core.Barcode> { public static class: java.lang.Class<com.miteksystems.misnap.core.Barcode..serializer>; // ❌ Double dots public static INSTANCE: com.miteksystems.misnap.core.Barcode..serializer; // ❌ Double dots } } }Root Cause
In
DtsApi.javaline 541, the code replaces$with.:String[] currParts = currClass.getClassName().replace('$', '.').split("\\.");For
Barcode$$serializer:
- Replace
$→.:Barcode..serializer- Split on
.:["Barcode", "", "serializer"]- Empty string in array creates
module {(invalid!)Impact
- 63 empty module declarations in test file
- 126 invalid double-dot type references
- TypeScript compilation fails
Proposed Fix
Add
$$serializerto the filtered class list inDtsApi.javaaround line 143:if (currentFileClassname.startsWith("java.util.function") || currentFileClassname.startsWith("android.support.v4.media.routing.MediaRouterJellybeanMr1") || currentFileClassname.startsWith("android.support.v4.media.routing.MediaRouterJellybeanMr2") || currentFileClassname.contains(".debugger.") || currentFileClassname.endsWith("package-info") || currentFileClassname.endsWith("module-info") || currentFileClassname.endsWith("Kt") || currentFileClassname.contains("$$serializer")) { // ← NEW LINE continue; }Why This Fix is Safe
$$serializerclasses are Kotlin compiler internals for serialization- They're never directly used by application code
- Filtering them is similar to filtering other compiler-generated classes (already done for
Ktsuffix)
Issue 2: Kotlin Synthetic Parameter Names Are Invalid TypeScript
Problem
Kotlin compiler generates synthetic setter methods with the parameter name
<set-?>in the LocalVariableTable. The DTS generator reads this directly, creating invalid TypeScript identifiers.Example from Bytecode (javap output)
public final void setInitialDelay(java.lang.Integer); LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/miteksystems/misnap/core/MiSnapSettings$Analysis; 0 6 1 <set-?> Ljava/lang/Integer;Generated TypeScript (Invalid)
public setInitialDelay(<set-?>: java.lang.Integer): void; // ❌ Invalid identifier public setEnableAiBasedRts(<set-?>: java.lang.Boolean): void; // ❌ Invalid identifier public setMotionDetectorSensitivity(<set-?>: com.miteksystems.misnap.core.MiSnapSettings.Analysis.MotionDetectorSensitivity): void; // ❌ Invalid identifierRoot Cause
In
DtsApi.javalines 962-969, the code reads parameter names from bytecode but doesn't sanitize Kotlin synthetic names:if (localVariable != null) { String name = localVariable.getName(); // No sanitization for <set-?> or other synthetic names! if (reservedJsKeywords.contains(name)) { sb.append(name + "_"); } else { sb.append(name); // Writes <set-?> directly } }Impact
- 68 occurrences in test file
- All Kotlin property setter methods affected
- TypeScript parser fails on angle bracket identifiers
Proposed Fix
Sanitize synthetic parameter names by deriving meaningful names from method names:
if (localVariable != null) { String name = localVariable.getName(); // Sanitize Kotlin synthetic parameter names like <set-?> if (name.startsWith("<") && name.endsWith(">")) { // For setter methods, derive parameter name from method name String methodName = m.getName(); if (methodName.startsWith("set") && methodName.length() > 3) { // setInitialDelay -> initialDelay name = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); } else { // Fallback to "value" for other synthetic names name = "value"; } } if (reservedJsKeywords.contains(name)) { System.out.println(String.format("Appending _ to reserved JS keyword %s", name)); sb.append(name + "_"); } else { sb.append(name); } }Result After Fix
public setInitialDelay(initialDelay: java.lang.Integer): void; // ✅ Valid and semantic! public setEnableAiBasedRts(enableAiBasedRts: java.lang.Boolean): void; // ✅ Valid and semantic! public setMotionDetectorSensitivity(motionDetectorSensitivity: com.miteksystems.misnap.core.MiSnapSettings.Analysis.MotionDetectorSensitivity): void; // ✅ Valid and semantic!Why This Fix is Safe
- Only affects synthetic Kotlin parameter names (those with
<and>)- Generates semantically meaningful names matching the property being set
- Follows common naming conventions (same pattern as Kotlin itself uses in Kotlin files)
Issue 3: Kotlin Type Generics Create Invalid
any<...>SyntaxProblem
Kotlin standard library types (
kotlin.jvm.functions.*,kotlin.properties.*) are in the "ignored namespaces" list and get replaced withany. However, the regex replacement preserves generic type parameters, creating invalid TypeScript syntax (anyis not a generic type).Example from Bytecode
public final class FragmentViewBindingDelegate<T extends ViewBinding> implements kotlin.properties.ReadOnlyProperty<androidx.fragment.app.Fragment, T>Generated TypeScript (Invalid)
export class FragmentViewBindingDelegate<T> extends any<androidx.fragment.app.Fragment,any> { // ❌ any is not generic! public getViewBindingFactory(): any<globalAndroid.view.View,any>; // ❌ Invalid syntax public constructor(fragment: androidx.fragment.app.Fragment, viewBindingFactory: any<any,any>); // ❌ Invalid syntax }Root Cause
In
DtsApi.javaline 248, the regex pattern doesn't include commas and spaces in the character class for matching generic parameters:String regexFormat = "(?<Replace>%s(?:(?:\\.[a-zA-Z\\d]*)|<[a-zA-Z\\d\\.<>]*>)*)(?<Suffix>[^a-zA-Z\\d]+)"; // ^^^^^^^^^^^^^^ Missing comma and space!When the regex encounters
kotlin.properties.ReadOnlyProperty<Fragment, T>:
- Matches up to:
kotlin.properties.ReadOnlyProperty- Encounters
<which is[^a-zA-Z\d], so it becomes the Suffix- Replacement:
any<+ remaining text- Result:
any<Fragment, T>(invalid!)Impact
- ~30 occurrences across various Kotlin function and property types
- All Kotlin lambda and property delegate types affected
- TypeScript compilation fails
Proposed Fix
Update the regex to include commas, spaces, and match end-of-string:
private String replaceIgnoredNamespaces(String content) { // Updated regex to properly capture generics with commas, spaces, and nested angle brackets // Also matches end of string or line as valid suffix String regexFormat = "(?<Replace>%s(?:(?:\\.[a-zA-Z\\d]*)|<[a-zA-Z\\d\\.<>, ]*>)*)(?<Suffix>[^a-zA-Z\\d]+|$)"; // ^^^^^ Added comma and space // ^^^ Added end-of-string for (String ignoredNamespace : this.getIgnoredNamespaces()) { String regexString = String.format(regexFormat, ignoredNamespace.replace(".", "\\.")); content = content.replaceAll(regexString, "any$2"); regexString = String.format(regexFormat, getGlobalAliasedClassName(ignoredNamespace).replace(".", "\\.")); content = content.replaceAll(regexString, "any$2"); } // replace "extends any" with "extends java.lang.Object" content = content.replace(" extends any ", String.format(" extends %s ", DtsApi.JavaLangObject)); return content; }Result After Fix
export class FragmentViewBindingDelegate<T> extends java.lang.Object { // ✅ Valid TypeScript! public getViewBindingFactory(): any; // ✅ Valid TypeScript! public constructor(fragment: androidx.fragment.app.Fragment, viewBindingFactory: any); // ✅ Valid TypeScript! }Why This Fix is Safe
- Only affects types in the ignored namespaces list (already being replaced with
any)- Properly strips all generic parameters when replacing, creating valid TypeScript
- The regex already existed; this just makes it work correctly for types with commas in generics
Testing Results
Test Environment
- Library: MiSnap SDK v5.8.1 (11 modules)
- Project: NativeScript mobile app
- TypeScript Version: As configured by NativeScript
Before Fixes
- ❌ Empty module declarations: 63
- ❌ Double-dot references: 126
- ❌ Invalid
<set-?>parameters: 68- ❌ Invalid
any<...>syntax: ~30- ❌ Total TypeScript errors: 287+
After Fixes
- ✅ Empty module declarations: 0
- ✅ Double-dot references: 0
- ✅ Invalid parameters: 0
- ✅ Invalid
any<...>syntax: 0- ✅ All type definitions compile successfully
- ✅ NativeScript project runs without issues
Generated File Sizes (Example: misnap-core.d.ts)
- Before: 3,789 lines with errors
- After: 3,096 lines (693 lines of invalid code removed)
- Reduction: 18.3% smaller, 100% valid
Compatibility
These fixes specifically target Kotlin-generated bytecode patterns that are:
- Standard Kotlin compiler output (not library-specific)
- Present in any modern Kotlin Android library
- Not handled by the current generator logic
The fixes are backward compatible - they only affect Kotlin libraries and don't change behavior for Java-only libraries.
Proposed Changes Summary
File:
dts-generator/src/main/java/com/telerik/dts/DtsApi.javaChange 1 (around line 143):
// Add to filtered classes list currentFileClassname.contains("$$serializer")Change 2 (lines 965-976, in
getMethodParamSignaturemethod):// Sanitize Kotlin synthetic parameter names if (name.startsWith("<") && name.endsWith(">")) { String methodName = m.getName(); if (methodName.startsWith("set") && methodName.length() > 3) { name = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4); } else { name = "value"; } }Change 3 (line 249, in
replaceIgnoredNamespacesmethod):// Update regex pattern String regexFormat = "(?<Replace>%s(?:(?:\\.[a-zA-Z\\d]*)|<[a-zA-Z\\d\\.<>, ]*>)*)(?<Suffix>[^a-zA-Z\\d]+|$)";
References
- Kotlin Serialization Docs: https://kotlinlang.org/docs/serialization.html
- Kotlin Bytecode Conventions: https://kotlinlang.org/spec/
- Related Issue: Similar to the 2016 fix for interface static initializers (commit 26a0870)
Tested With:
- MiSnap SDK v5.8.1 (Mitek Systems)
- NativeScript 8.x
- Multiple Kotlin-based AndroidX libraries
Let me know if I should commit the changes so that they can be tested :)