Ctrl K
ComunidadGuías…

tests/test_4874_jvm_resolution.py

Rama v8 · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""#4874: Java/Kotlin references must reach the declaration node, not a file or stub.

The fixture is the issue's four-file project. The tests split the contract into
the three reported failures plus controls that must keep passing:

  * Kotlin imports a Java class used only in a static call.
  * Java imports and extends a Kotlin class.
  * Kotlin calls a static Java method through that class.
  * `affected` must reach both language sides through the class node.
  * A Java/Kotlin import with a local definition must not be marked external.
  * An external import must not bind to a same-named local class.
  * Two same-named classes in different packages must be disambiguated by import.
  * A JVM call must not bind to a non-JVM definition.
"""
from __future__ import annotations

from pathlib import Path

from graphify.affected import affected_nodes, resolve_seed
from graphify.build import build_from_json
from graphify.extract import extract


PKG = {
    "Logger.java": """\
package com.example.util;
public class Logger { public static void info(String m) { System.out.println(m); } }
""",
    "BaseScreen.kt": """\
package com.example.ui
open class BaseScreen { fun show() {} }
""",
    "JavaUser.java": """\
package com.example.app;
import com.example.util.Logger;
import com.example.ui.BaseScreen;
public class JavaUser extends BaseScreen { public void run() { Logger.info("java"); } }
""",
    "KotlinUser.kt": """\
package com.example.app
import com.example.util.Logger
import com.example.ui.BaseScreen
class KotlinUser : BaseScreen() { fun run() { Logger.info("kotlin") } }
""",
}


def _write(root: Path, name: str, text: str) -> Path:
    path = root / "src" / name
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(text, encoding="utf-8")
    return path


def _extract(root: Path, files: dict[str, str]) -> dict:
    paths = [_write(root, name, text) for name, text in files.items()]
    return extract(paths, root=root, cache_root=root / "cache", parallel=False)


def _node(result: dict, label: str, suffix: str) -> dict:
    matches = [
        n for n in result["nodes"]
        if n.get("label") == label
        and str(n.get("source_file", "")).replace("\\", "/").endswith(suffix)
    ]
    assert len(matches) == 1, (label, suffix, matches)
    return matches[0]


def _edges(result: dict, relation: str) -> list[dict]:
    return [e for e in result["edges"] if e.get("relation") == relation]


def _edge(result: dict, source: str, relation: str, target: str) -> dict | None:
    for edge in _edges(result, relation):
        if edge.get("source") == source and edge.get("target") == target:
            return edge
    return None


def _sourceless_ids(result: dict) -> set[str]:
    return {n["id"] for n in result["nodes"] if not n.get("source_file")}


def _affected_labels(result: dict, seed: str, depth: int = 2) -> set[str]:
    graph = build_from_json(result, directed=True)
    seed_id = resolve_seed(graph, seed)
    assert seed_id is not None, f"seed {seed!r} did not resolve"
    hits = affected_nodes(graph, seed_id, depth=depth)
    return {str(graph.nodes[hit.node_id].get("label", hit.node_id)) for hit in hits}


def test_kotlin_import_of_java_class_targets_the_class_node(tmp_path: Path) -> None:
    result = _extract(tmp_path, PKG)
    kotlin_file = _node(result, "KotlinUser.kt", "KotlinUser.kt")
    logger_class = _node(result, "Logger", "Logger.java")
    logger_file = _node(result, "Logger.java", "Logger.java")

    assert _edge(result, kotlin_file["id"], "imports", logger_class["id"]), (
        "Kotlin's import of Logger must reach the Logger class node"
    )
    assert logger_class["id"] != logger_file["id"]


def test_java_import_and_inheritance_of_kotlin_class_target_the_class_node(tmp_path: Path) -> None:
    result = _extract(tmp_path, PKG)
    java_file = _node(result, "JavaUser.java", "JavaUser.java")
    java_class = _node(result, "JavaUser", "JavaUser.java")
    base_class = _node(result, "BaseScreen", "BaseScreen.kt")

    assert _edge(result, java_file["id"], "imports", base_class["id"]), (
        "Java's import of BaseScreen must reach the Kotlin class node"
    )
    assert _edge(result, java_class["id"], "inherits", base_class["id"]), (
        "JavaUser extends BaseScreen must reach the Kotlin class node"
    )
    assert not any(
        e.get("target") in _sourceless_ids(result)
        for e in _edges(result, "imports")
        if e.get("source") == java_file["id"]
    ), "the Java import must not be parked on a source-less FQN stub"


def test_kotlin_member_call_reaches_the_java_method(tmp_path: Path) -> None:
    result = _extract(tmp_path, PKG)
    kotlin_run = _node(result, ".run()", "KotlinUser.kt")
    logger_info = _node(result, ".info()", "Logger.java")

    assert _edge(result, kotlin_run["id"], "calls", logger_info["id"]), (
        "KotlinUser.run() must call Logger.info() across the JVM language boundary"
    )


def test_affected_reaches_dependents_in_both_languages(tmp_path: Path) -> None:
    result = _extract(tmp_path, PKG)

    logger_hits = _affected_labels(result, "Logger", depth=2)
    assert {"JavaUser.java", "KotlinUser.kt"} <= logger_hits, logger_hits

    base_hits = _affected_labels(result, "BaseScreen", depth=2)
    assert {"JavaUser.java", "KotlinUser.kt"} <= base_hits, base_hits


def test_existing_kotlin_subclass_of_java_base_is_unchanged(tmp_path: Path) -> None:
    files = {
        "Base.java": "package models;\npublic class Base {}\n",
        "Child.kt": "package app\nimport models.Base\nclass Child : Base()\n",
    }
    result = _extract(tmp_path, files)
    child = _node(result, "Child", "Child.kt")
    base = _node(result, "Base", "Base.java")
    assert _edge(result, child["id"], "inherits", base["id"])


def test_java_to_kotlin_import_without_inheritance_targets_class(tmp_path: Path) -> None:
    files = {
        "Widget.kt": "package ui\nclass Widget\n",
        "Use.java": (
            "package app;\n"
            "import ui.Widget;\n"
            "public class Use { Widget field; }\n"
        ),
    }
    result = _extract(tmp_path, files)
    use_file = _node(result, "Use.java", "Use.java")
    widget = _node(result, "Widget", "Widget.kt")
    assert _edge(result, use_file["id"], "imports", widget["id"])


def test_external_java_import_does_not_bind_to_a_local_same_name_class(tmp_path: Path) -> None:
    files = {
        "Component.java": "package local;\npublic class Component {}\n",
        "Use.java": (
            "package app;\n"
            "import org.springframework.stereotype.Component;\n"
            "public class Use { Component field; }\n"
        ),
    }
    result = _extract(tmp_path, files)
    local = _node(result, "Component", "Component.java")
    use_file = _node(result, "Use.java", "Use.java")
    assert not _edge(result, use_file["id"], "imports", local["id"]), (
        "an explicit external import must not collapse onto a local same-named class"
    )


def test_same_simple_name_in_two_packages_uses_the_import_package(tmp_path: Path) -> None:
    files = {
        "a/Logger.java": "package a;\npublic class Logger {}\n",
        "b/Logger.java": "package b;\npublic class Logger {}\n",
        "Use.kt": (
            "package app\n"
            "import b.Logger\n"
            "class Use { fun f() { Logger.info() } }\n"
        ),
    }
    result = _extract(tmp_path, files)
    kotlin_file = _node(result, "Use.kt", "Use.kt")
    logger_a = _node(result, "Logger", "a/Logger.java")
    logger_b = _node(result, "Logger", "b/Logger.java")
    assert _edge(result, kotlin_file["id"], "imports", logger_b["id"])
    assert not _edge(result, kotlin_file["id"], "imports", logger_a["id"])


def test_jvm_call_does_not_bind_to_a_non_jvm_type(tmp_path: Path) -> None:
    files = {
        "logger.py": "class Logger:\n    def info(self):\n        return None\n",
        "Use.kt": "package app\nclass Use { fun f() { Logger.info() } }\n",
    }
    result = _extract(tmp_path, files)
    kotlin_run = _node(result, ".f()", "Use.kt")
    python_info = [n for n in result["nodes"] if n.get("label") == ".info()"]
    assert python_info, "fixture must contain the Python method node"
    assert not _edge(result, kotlin_run["id"], "calls", python_info[0]["id"])