Skip to content

Conversation

dangrima90
Copy link

As highlighted in #84, I encountered issues whilst trying to generate types for AARs built with Kotlin. I investigated the issues via Claude Code. The following description was also generated with Claude Code to ensure that the issues found and how they were fixed are documented 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.serialization compiler 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.class

Generated 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.java line 541, the code replaces $ with .:

String[] currParts = currClass.getClassName().replace('$', '.').split("\\.");

For Barcode$$serializer:

  1. Replace $.: Barcode..serializer
  2. Split on .: ["Barcode", "", "serializer"]
  3. 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 $$serializer to the filtered class list in DtsApi.java around 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

  • $$serializer classes 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 Kt suffix)

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 identifier

Root Cause

In DtsApi.java lines 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<...> Syntax

Problem

Kotlin standard library types (kotlin.jvm.functions.*, kotlin.properties.*) are in the "ignored namespaces" list and get replaced with any. However, the regex replacement preserves generic type parameters, creating invalid TypeScript syntax (any is 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.java line 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>:

  1. Matches up to: kotlin.properties.ReadOnlyProperty
  2. Encounters < which is [^a-zA-Z\d], so it becomes the Suffix
  3. Replacement: any< + remaining text
  4. 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.java

Change 1 (around line 143):

// Add to filtered classes list
currentFileClassname.contains("$$serializer")

Change 2 (lines 965-976, in getMethodParamSignature method):

// 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 replaceIgnoredNamespaces method):

// Update regex pattern
String regexFormat = "(?<Replace>%s(?:(?:\\.[a-zA-Z\\d]*)|<[a-zA-Z\\d\\.<>, ]*>)*)(?<Suffix>[^a-zA-Z\\d]+|$)";

References


Tested With:

  • MiSnap SDK v5.8.1 (Mitek Systems)
  • NativeScript 8.x
  • Multiple Kotlin-based AndroidX libraries

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant