Skip to content

Kotlin: cross-file calls to object (singleton) members produce no calls edges — same-file resolution works #1698

Description

@jerryliurui

Environment

  • graphify 0.9.7 (PyPI graphifyy), macOS, Python 3.10
  • Extraction invoked directly via graphify.extract.extract(files, parallel=False) (same result through the CLI pipeline)

Summary

Calls to members of a Kotlin object (singleton) resolve to calls edges only when caller and callee are in the same file. The identical pattern across two files produces zero edges — not even a references edge from the caller's class to the object.

This is the Kotlin sibling of the Swift navigation-expression work that landed in 0.9.5 (#1604) and looks adjacent to #1696 (Java receivers) and #1682 (PHP member calls).

Reproducer (3 files)

MessageHelper.kt

package demo

object MessageHelper {
    fun isRoomClosed(roomId: String): Boolean {
        return roomId.isEmpty()
    }
}

RoomPage.kt

package demo

class RoomPage {
    fun refresh(roomId: String) {
        if (MessageHelper.isRoomClosed(roomId)) {
            return
        }
    }
}

SameFile.kt (control — same pattern, single file)

package demo

object LocalHelper {
    fun isReady(): Boolean = true
}

class LocalPage {
    fun open() {
        if (LocalHelper.isReady()) {
            return
        }
    }
}

Current behavior (0.9.7)

Same-file control resolves correctly:

samefile_localpage_open --calls--> samefile_localhelper_isready  [EXTRACTED]

Cross-file case emits nothing. roompage_roompage_refresh has only:

roompage_roompage --method--> roompage_roompage_refresh          [EXTRACTED]
roompage_roompage_refresh --references--> roompage_kt_string     [EXTRACTED]

No calls edge to messagehelper_messagehelper_isroomclosed, and no references edge to MessageHelper at all.

Expected

roompage_roompage_refresh --calls--> messagehelper_messagehelper_isroomclosed  [EXTRACTED or INFERRED]

The receiver here is a navigation-qualified object name — statically unambiguous, no type inference needed, so this should be the easiest cross-file case for Kotlin.

Real-world impact

Found while indexing a large production Android monorepo (~5,900 Kotlin/Java files). A messaging utility object whose method is called from 3 different files (verified by grep and by a Hybrid-LSP-based tool, which both report all 3 callers) shows zero incoming calls edges in the graph, so "who calls X" silently returns an empty answer instead of failing loudly. Kotlin object + top-level utility functions are the dominant utility pattern in Android codebases, so this gap affects a large share of caller-lookup queries.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions