diff --git a/java/gazelle/testdata/kt_fqn_deps/BUILD.in b/java/gazelle/testdata/kt_fqn_deps/BUILD.in new file mode 100644 index 00000000..feccf582 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/BUILD.in @@ -0,0 +1 @@ +# gazelle:jvm_kotlin_enabled true diff --git a/java/gazelle/testdata/kt_fqn_deps/BUILD.out b/java/gazelle/testdata/kt_fqn_deps/BUILD.out new file mode 100644 index 00000000..feccf582 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/BUILD.out @@ -0,0 +1 @@ +# gazelle:jvm_kotlin_enabled true diff --git a/java/gazelle/testdata/kt_fqn_deps/README.md b/java/gazelle/testdata/kt_fqn_deps/README.md new file mode 100644 index 00000000..9a93eb0f --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/README.md @@ -0,0 +1,13 @@ +# Kotlin fully qualified name dependencies + +Test that Gazelle detects dependencies from fully qualified class references +used directly in Kotlin expressions, without corresponding import statements. + +When Kotlin code uses FQN constructor calls like +`com.example.errors.CustomError(e)` instead of importing `CustomError`, the +parser must still recognize the cross-package dependency. This is common when +there are name conflicts (e.g., a custom `InterruptedException` alongside +`java.util.concurrent.InterruptedException`). + +The `app/src` package uses a FQN constructor call to `com.example.errors.CustomError` +and should get a `deps` entry for `//errors/src`. diff --git a/java/gazelle/testdata/kt_fqn_deps/WORKSPACE b/java/gazelle/testdata/kt_fqn_deps/WORKSPACE new file mode 100644 index 00000000..b16824a2 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/WORKSPACE @@ -0,0 +1,19 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +rules_kotlin_version = "1.9.6" + +rules_kotlin_sha = "3b772976fec7bdcda1d84b9d39b176589424c047eb2175bed09aac630e50af43" + +http_archive( + name = "rules_kotlin", + sha256 = rules_kotlin_sha, + urls = ["https://github.com/bazelbuild/rules_kotlin/releases/download/v%s/rules_kotlin-v%s.tar.gz" % (rules_kotlin_version, rules_kotlin_version)], +) + +load("@rules_kotlin//kotlin:repositories.bzl", "kotlin_repositories") + +kotlin_repositories() + +load("@rules_kotlin//kotlin:core.bzl", "kt_register_toolchains") + +kt_register_toolchains() diff --git a/java/gazelle/testdata/kt_fqn_deps/app/src/App.kt b/java/gazelle/testdata/kt_fqn_deps/app/src/App.kt new file mode 100644 index 00000000..8f00da3a --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/app/src/App.kt @@ -0,0 +1,9 @@ +package com.example.app + +fun doWork() { + try { + // work + } catch (e: Exception) { + throw com.example.errors.CustomError(e) + } +} diff --git a/java/gazelle/testdata/kt_fqn_deps/app/src/BUILD.in b/java/gazelle/testdata/kt_fqn_deps/app/src/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/kt_fqn_deps/app/src/BUILD.out b/java/gazelle/testdata/kt_fqn_deps/app/src/BUILD.out new file mode 100644 index 00000000..7282c949 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/app/src/BUILD.out @@ -0,0 +1,8 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "src", + srcs = ["App.kt"], + visibility = ["//:__subpackages__"], + deps = ["//errors/src"], +) diff --git a/java/gazelle/testdata/kt_fqn_deps/errors/src/BUILD.in b/java/gazelle/testdata/kt_fqn_deps/errors/src/BUILD.in new file mode 100644 index 00000000..e69de29b diff --git a/java/gazelle/testdata/kt_fqn_deps/errors/src/BUILD.out b/java/gazelle/testdata/kt_fqn_deps/errors/src/BUILD.out new file mode 100644 index 00000000..9c38b013 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/errors/src/BUILD.out @@ -0,0 +1,7 @@ +load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") + +kt_jvm_library( + name = "src", + srcs = ["CustomError.kt"], + visibility = ["//:__subpackages__"], +) diff --git a/java/gazelle/testdata/kt_fqn_deps/errors/src/CustomError.kt b/java/gazelle/testdata/kt_fqn_deps/errors/src/CustomError.kt new file mode 100644 index 00000000..36341fad --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/errors/src/CustomError.kt @@ -0,0 +1,3 @@ +package com.example.errors + +class CustomError(cause: Exception) : RuntimeException(cause) diff --git a/java/gazelle/testdata/kt_fqn_deps/maven_install.json b/java/gazelle/testdata/kt_fqn_deps/maven_install.json new file mode 100644 index 00000000..6c01c298 --- /dev/null +++ b/java/gazelle/testdata/kt_fqn_deps/maven_install.json @@ -0,0 +1 @@ +{"version": "2"} diff --git a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParser.java b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParser.java index 841449f7..96ddfa58 100644 --- a/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParser.java +++ b/java/src/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParser.java @@ -605,6 +605,29 @@ public void visitDotQualifiedExpression(KtDotQualifiedExpression expression) { String functionName = callExpr.getCalleeExpression().getText(); checkExtensionFunctionCall(receiverType, functionName); + + // Detect FQN constructor call: com.example.ClassName(args) + // Mirrors ClasspathParser.visitNewClass → checkFullyQualifiedType + if (isLikelyClassName(functionName) && receiverType.contains(".")) { + String fqClassName = receiverType + "." + functionName; + packageData.usedTypes.add(fqClassName); + } + } + } + + // Detect FQN class reference: com.example.ClassName (as selector of a DQE) + // Mirrors ClasspathParser.visitMethodInvocation + looksLikeClassName + if (selectorExpression instanceof KtSimpleNameExpression) { + String selectorName = ((KtSimpleNameExpression) selectorExpression).getReferencedName(); + if (isLikelyClassName(selectorName)) { + KtExpression receiverExpr = expression.getReceiverExpression(); + if (receiverExpr != null) { + String receiverText = receiverExpr.getText(); + if (receiverText.contains(".")) { + String fqClassName = receiverText + "." + selectorName; + packageData.usedTypes.add(fqClassName); + } + } } } diff --git a/java/test/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParserTest.java b/java/test/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParserTest.java index 7395145f..b8ca7a6b 100644 --- a/java/test/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParserTest.java +++ b/java/test/com/github/bazel_contrib/contrib_rules_jvm/javaparser/generators/KtParserTest.java @@ -148,22 +148,25 @@ public void constantTest() throws IOException { data.perClassData.keySet()); } - // @Test - // public void fullyQualifiedClassAndFunctionUse() throws IOException { - // ParsedPackageData data = parser.parseClasses(getPathsWithNames("FullyQualifieds.kt")); - // assertEquals( - // Set.of("com.example"), - // data.usedPackagesWithoutSpecificTypes); - // assertEquals( - // Set.of( - // "workspace.com.gazelle.java.javaparser.generators.DeleteBookRequest", - // "workspace.com.gazelle.java.javaparser.generators.DeleteBookResponse", - // "workspace.com.gazelle.java.javaparser.utils.Printer", - // "workspace.com.gazelle.java.javaparser.factories.Factory", - // "java.util.ArrayList", - // "com.example.PrivateArg"), - // data.usedTypes); - // } + @Test + public void detectsFqnClassReferences() throws IOException { + ParsedPackageData data = parser.parseClasses(getPathsWithNames("FullyQualifieds.kt")); + + // FQN constructor calls and class references detected via visitDotQualifiedExpression + assertTrue( + data.usedTypes.contains( + "workspace.com.gazelle.java.javaparser.generators.DeleteBookRequest"), + "Should detect FQN constructor call: " + data.usedTypes); + assertTrue( + data.usedTypes.contains("workspace.com.gazelle.java.javaparser.utils.Printer"), + "Should detect FQN class reference (static method call): " + data.usedTypes); + assertTrue( + data.usedTypes.contains("workspace.com.gazelle.java.javaparser.factories.Factory"), + "Should detect FQN class reference (factory call): " + data.usedTypes); + assertTrue( + data.usedTypes.contains("java.util.ArrayList"), + "Should detect FQN generic constructor call: " + data.usedTypes); + } @Test public void staticImportsTest() throws IOException {