Merge pull request #72 from bazelbuild/friends

Friends support
diff --git a/README.md b/README.md
index 735467c..2c7b8a0 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,9 @@
 [Skydoc documentation](https://bazelbuild.github.io/rules_kotlin)
 
 # Announcements
-* <b>February 15, 2018.</b>. Toolchains for the JVM rules. Currently this allow tweaking: 
+* <b>May 25, 2018.<b> Test "friend" support. A single friend dep can be provided to `kt_jvm_test` which allows the test
+  to access internal members of the module under test.
+* <b>February 15, 2018.</b> Toolchains for the JVM rules. Currently this allow tweaking: 
     * The JVM target (bytecode level).
     * API and Language levels.
     * Coroutines, enabled by default. 
diff --git a/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar b/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
index 97f045d..3ffaa7c 100755
--- a/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
+++ b/kotlin/builder/proto/jars/libkotlin_model_proto-speed.jar
Binary files differ
diff --git a/kotlin/builder/proto/kotlin_model.proto b/kotlin/builder/proto/kotlin_model.proto
index 7fdfe16..30ddb01 100644
--- a/kotlin/builder/proto/kotlin_model.proto
+++ b/kotlin/builder/proto/kotlin_model.proto
@@ -69,6 +69,9 @@
 
         // derived from plugins
         repeated string encoded_plugin_descriptors=9;
+
+        // friend jars -- kotlin compiler allows internal visibility access to these jars. Used for tests.
+        repeated string friend_paths = 10;
     }
 
     message Outputs {
diff --git a/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt b/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
index b31b31d..58a46d0 100644
--- a/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
+++ b/kotlin/builder/src/io/bazel/kotlin/builder/BuildCommandBuilder.kt
@@ -114,8 +114,11 @@
             with(root.infoBuilder) {
                 label = argMap.mandatorySingle(JavaBuilderFlags.TARGET_LABEL.flag)
                 ruleKind = argMap.mandatorySingle(JavaBuilderFlags.RULE_KIND.flag)
-                kotlinModuleName = argMap.optionalSingle("--kotlin_module_name")
+                kotlinModuleName = argMap.mandatorySingle("--kotlin_module_name").also {
+                    check(it.isNotBlank()) { "--kotlin_module_name should not be blank" }
+                }
                 passthroughFlags = argMap.optionalSingle("--kotlin_passthrough_flags")
+                addAllFriendPaths(argMap.mandatory("--kotlin_friend_paths"))
                 toolchainInfoBuilder.commonBuilder.apiVersion = argMap.mandatorySingle("--kotlin_api_version")
                 toolchainInfoBuilder.commonBuilder.languageVersion = argMap.mandatorySingle("--kotlin_language_version")
                 toolchainInfoBuilder.jvmBuilder.jvmTarget = argMap.mandatorySingle("--kotlin_jvm_target")
@@ -136,10 +139,6 @@
                     `package` = it[0]
                     target = it[1]
                 }
-
-                kotlinModuleName = kotlinModuleName.supplyIfNullOrBlank {
-                    "${`package`.trimStart { it == '/' }.replace('/', '_')}-$target"
-                }
             }
             root.build()
         }
diff --git a/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/KotlinCompiler.kt b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/KotlinCompiler.kt
index d8b9042..dbff055 100644
--- a/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/KotlinCompiler.kt
+++ b/kotlin/builder/src/io/bazel/kotlin/builder/mode/jvm/actions/KotlinCompiler.kt
@@ -59,11 +59,14 @@
     private fun setupCompileContext(command: KotlinModel.BuilderCommand): MutableList<String> {
         val args = mutableListOf<String>()
 
+        // use -- for flags not meant for the kotlin compiler
         args.addAll(
             "-cp", command.inputs.joinedClasspath,
             "-api-version", command.info.toolchainInfo.common.apiVersion,
             "-language-version", command.info.toolchainInfo.common.languageVersion,
-            "-jvm-target", command.info.toolchainInfo.jvm.jvmTarget
+            "-jvm-target", command.info.toolchainInfo.jvm.jvmTarget,
+            // https://github.com/bazelbuild/rules_kotlin/issues/69: remove once jetbrains adds a flag for it.
+            "--friend-paths", command.info.friendPathsList.joinToString(":")
         )
 
         args
diff --git a/kotlin/builder/src/io/bazel/kotlin/compiler/BazelK2JVMCompiler.kt b/kotlin/builder/src/io/bazel/kotlin/compiler/BazelK2JVMCompiler.kt
index b42af31..e4d104d 100644
--- a/kotlin/builder/src/io/bazel/kotlin/compiler/BazelK2JVMCompiler.kt
+++ b/kotlin/builder/src/io/bazel/kotlin/compiler/BazelK2JVMCompiler.kt
@@ -16,10 +16,40 @@
 package io.bazel.kotlin.compiler
 
 import org.jetbrains.kotlin.cli.common.ExitCode
+import org.jetbrains.kotlin.cli.common.messages.MessageRenderer
+import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector
 import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+import org.jetbrains.kotlin.config.Services
 
+@Suppress("unused")
 class BazelK2JVMCompiler(private val delegate: K2JVMCompiler = K2JVMCompiler()) {
+    private lateinit var friendsPaths: Array<String>
+
+    private fun preprocessArgs(args: Array<out String>): Array<out String> {
+        val tally = mutableListOf<String>()
+        var i =0
+        do {
+            when {
+                // https://github.com/bazelbuild/rules_kotlin/issues/69: remove once jetbrains adds a flag for it.
+                args[i].startsWith("--friend-paths") -> {
+                    i++
+                    friendsPaths = args[i].split(":").toTypedArray()
+                }
+                else -> tally += args[i]
+            }
+            i++
+        } while(i < args.size)
+        return tally.toTypedArray()
+    }
+
     fun exec(errStream: java.io.PrintStream, vararg args: kotlin.String): ExitCode {
-        return delegate.exec(errStream, *args)
+        val arguments = delegate.createArguments().also {
+            delegate.parseArguments(preprocessArgs(args), it)
+            if(::friendsPaths.isInitialized) {
+                it.friendPaths = friendsPaths
+            }
+        }
+        val collector = PrintingMessageCollector(errStream, MessageRenderer.PLAIN_RELATIVE_PATHS, arguments.verbose)
+        return delegate.exec(collector, Services.EMPTY, arguments)
     }
 }
\ No newline at end of file
diff --git a/kotlin/internal/compile.bzl b/kotlin/internal/compile.bzl
index 1fa2ee9..6cabade 100644
--- a/kotlin/internal/compile.bzl
+++ b/kotlin/internal/compile.bzl
@@ -18,7 +18,7 @@
 _src_file_types = FileType([".java", ".kt"])
 _srcjar_file_type = FileType([".srcjar"])
 
-def _kotlin_do_compile_action(ctx, rule_kind, output_jar, compile_jars):
+def _kotlin_do_compile_action(ctx, rule_kind, output_jar, compile_jars, module_name, friend_paths):
     """Internal macro that sets up a Kotlin compile action.
 
     This macro only supports a single Kotlin compile operation for a rule.
@@ -47,10 +47,11 @@
         "--output", output_jar.path,
         "--output_jdeps", ctx.outputs.jdeps.path,
         "--classpath", "\n".join([f.path for f in compile_jars.to_list()]),
+        "--kotlin_friend_paths", "\n".join(friend_paths.to_list()),
         "--kotlin_jvm_target", tc.jvm_target,
         "--kotlin_api_version", tc.api_version,
         "--kotlin_language_version", tc.language_version,
-        "--kotlin_module_name", getattr(ctx.attr, "module_name", ""),
+        "--kotlin_module_name", module_name,
         "--kotlin_passthrough_flags", "-Xcoroutines=%s" % tc.coroutines
     ]
 
@@ -98,7 +99,7 @@
 def _select_std_libs(ctx):
     return ctx.files._kotlin_std
 
-def _make_java_provider(ctx, auto_deps=[]):
+def _make_java_provider(ctx, input_deps=[], auto_deps=[]):
     """Creates the java_provider for a Kotlin target.
 
     This macro is distinct from the kotlin_make_providers as collecting the java_info is useful before the DefaultInfo is
@@ -114,7 +115,7 @@
     Returns:
     A JavaInfo provider.
     """
-    deps=utils.collect_all_jars(ctx.attr.deps)
+    deps=utils.collect_all_jars(input_deps)
     exported_deps=utils.collect_all_jars(getattr(ctx.attr, "exports", []))
 
     my_compile_jars = exported_deps.compile_jars + [ctx.outputs.jar]
@@ -140,9 +141,10 @@
         transitive_runtime_jars=my_transitive_runtime_jars
     )
 
-def _make_providers(ctx, java_info, transitive_files=depset(order="default")):
+def _make_providers(ctx, java_info, module_name, transitive_files=depset(order="default")):
     kotlin_info=kt.info.KtInfo(
         srcs=ctx.files.srcs,
+        module_name = module_name,
         # intelij aspect needs this.
         outputs = struct(
             jdeps = ctx.outputs.jdeps,
@@ -167,7 +169,7 @@
         providers=[java_info,default_info,kotlin_info],
     )
 
-def _compile_action (ctx, rule_kind):
+def _compile_action(ctx, rule_kind, module_name, friend_paths=depset()):
     """Setup a kotlin compile action.
 
     Args:
@@ -196,12 +198,16 @@
 
     kotlin_auto_deps=_select_std_libs(ctx)
 
+    deps = ctx.attr.deps + getattr(ctx.attr, "friends", [])
+
     # setup the compile action.
     _kotlin_do_compile_action(
         ctx,
         rule_kind = rule_kind,
         output_jar = kt_compile_output_jar,
-        compile_jars = utils.collect_jars_for_compile(ctx.attr.deps) + kotlin_auto_deps
+        compile_jars = utils.collect_jars_for_compile(deps) + kotlin_auto_deps,
+        module_name = module_name,
+        friend_paths = friend_paths
     )
 
     # setup the merge action if needed.
@@ -209,7 +215,7 @@
         utils.actions.fold_jars(ctx, output_jar, output_merge_list)
 
     # create the java provider but the kotlin and default provider cannot be created here.
-    return _make_java_provider(ctx, kotlin_auto_deps)
+    return _make_java_provider(ctx, deps, kotlin_auto_deps)
 
 compile = struct(
     compile_action = _compile_action,
diff --git a/kotlin/internal/kt.bzl b/kotlin/internal/kt.bzl
index 187e077..c660e70 100644
--- a/kotlin/internal/kt.bzl
+++ b/kotlin/internal/kt.bzl
@@ -26,6 +26,7 @@
 _KtInfo = provider(
     fields = {
         "srcs": "the source files. [intelij-aspect]",
+        "module_name": "the module name",
         "outputs": "output jars produced by this rule. [intelij-aspect]",
     },
 )
diff --git a/kotlin/internal/rules.bzl b/kotlin/internal/rules.bzl
index 2647ff0..79ee8b1 100644
--- a/kotlin/internal/rules.bzl
+++ b/kotlin/internal/rules.bzl
@@ -76,10 +76,16 @@
     return struct(kt = kotlin_info, providers= [default_info, java_info, kotlin_info])
 
 def kt_jvm_library_impl(ctx):
-    return compile.make_providers(ctx, compile.compile_action(ctx, "kt_jvm_library"))
+    module_name=utils.derive_module_name(ctx)
+    return compile.make_providers(
+        ctx,
+        compile.compile_action(ctx, "kt_jvm_library", module_name),
+        module_name,
+  )
 
 def kt_jvm_binary_impl(ctx):
-    java_info = compile.compile_action(ctx, "kt_jvm_binary")
+    module_name=utils.derive_module_name(ctx)
+    java_info = compile.compile_action(ctx, "kt_jvm_binary", module_name)
     utils.actions.write_launcher(
         ctx,
         java_info.transitive_runtime_jars,
@@ -89,15 +95,29 @@
     return compile.make_providers(
         ctx,
         java_info,
+        module_name,
         depset(
             order = "default",
             transitive=[java_info.transitive_runtime_jars],
             direct=[ctx.executable._java]
-        )
+        ),
     )
 
 def kt_jvm_junit_test_impl(ctx):
-    java_info = compile.compile_action(ctx, "kt_jvm_test")
+    module_name=utils.derive_module_name(ctx)
+    friend_paths=depset()
+
+    friends=getattr(ctx.attr, "friends", [])
+    if len(friends) > 1:
+        fail("only one friend is possible")
+    elif len(friends) == 1:
+        if friends[0][kt.info.KtInfo] == None:
+            fail("only kotlin dependencies can be friends")
+        else:
+            friend_paths += [j.path for j in friends[0][JavaInfo].compile_jars]
+            module_name = friends[0][kt.info.KtInfo].module_name
+
+    java_info = compile.compile_action(ctx, "kt_jvm_test", module_name,friend_paths)
 
     transitive_runtime_jars = java_info.transitive_runtime_jars + ctx.files._bazel_test_runner
     launcherJvmFlags = ["-ea", "-Dbazel.test_suite=%s"% ctx.attr.test_class]
@@ -111,9 +131,10 @@
     return compile.make_providers(
         ctx,
         java_info,
+        module_name,
         depset(
             order = "default",
             transitive=[transitive_runtime_jars],
             direct=[ctx.executable._java]
-        )
+        ),
     )
\ No newline at end of file
diff --git a/kotlin/internal/utils.bzl b/kotlin/internal/utils.bzl
index 7b6ce28..d7890a4 100644
--- a/kotlin/internal/utils.bzl
+++ b/kotlin/internal/utils.bzl
@@ -24,6 +24,12 @@
         lbl = lbl.replace("external/", "@")
     return lbl + "//" + l.package + ":" + l.name
 
+def _derive_module_name(ctx):
+    module_name=getattr(ctx.attr, "module_name", "")
+    if module_name == "":
+        module_name = (ctx.label.package.lstrip("/").replace("/","_") + "-" + ctx.label.name.replace("/", "_"))
+    return module_name
+
 # DEPSET UTILS #################################################################################################################################################
 def _select_compile_jars(dep):
     """selects the correct compile time jar from a java provider"""
@@ -207,4 +213,5 @@
     collect_all_jars = _collect_all_jars,
     collect_jars_for_compile = _collect_jars_for_compile,
     restore_label = _restore_label,
+    derive_module_name = _derive_module_name
 )
diff --git a/kotlin/kotlin.bzl b/kotlin/kotlin.bzl
index e8b3c5d..86248a8 100644
--- a/kotlin/kotlin.bzl
+++ b/kotlin/kotlin.bzl
@@ -301,7 +301,9 @@
 """
 
 kt_jvm_binary = rule(
-    attrs = dict(_runnable_common_attr.items() + {"main_class": attr.string(mandatory = True)}.items()),
+    attrs = dict(_runnable_common_attr.items() + {
+        "main_class": attr.string(mandatory = True)
+    }.items()),
     executable = True,
     outputs = _binary_outputs,
     toolchains = [_kt.defs.TOOLCHAIN_TYPE],
@@ -325,6 +327,9 @@
             default = Label("@bazel_tools//tools/jdk:TestRunner_deploy.jar"),
             allow_files = True,
         ),
+        "friends": attr.label_list(
+            default = [],
+        ),
         "test_class": attr.string(),
         "main_class": attr.string(default="com.google.testing.junit.runner.BazelTestRunner"),
     }.items()),
@@ -342,6 +347,8 @@
 
 Args:
   test_class: The Java class to be loaded by the test runner.
+  friends: A single Kotlin dep which allows the test code access to internal members. Currently uses the output jar of
+    the module -- i.e., exported deps won't be included.
 """
 
 kt_jvm_import = rule(
diff --git a/tests/integrationtests/BUILD b/tests/integrationtests/BUILD
index c0356b1..e985ee0 100644
--- a/tests/integrationtests/BUILD
+++ b/tests/integrationtests/BUILD
@@ -18,5 +18,6 @@
     tests=[
         "//tests/integrationtests/jvm:basic_tests",
         "//tests/integrationtests/jvm:annoation_processing_tests",
+        "//tests/integrationtests/jvm/basic:test_friends_tests"
     ]
 )
\ No newline at end of file
diff --git a/tests/integrationtests/jvm/basic/BUILD b/tests/integrationtests/jvm/basic/BUILD
index 6b200b9..29baf44 100644
--- a/tests/integrationtests/jvm/basic/BUILD
+++ b/tests/integrationtests/jvm/basic/BUILD
@@ -98,6 +98,22 @@
     deps = [":propagation_test_runtime_lib"]
 )
 
+kt_jvm_library(
+    name = "test_friends_library",
+    srcs = ["test_friends/Service.kt"]
+)
+
+# This test should be explicetly executed as module name mangling handling could regress otherwise.
+kt_jvm_test(
+    name = "test_friends_tests",
+    srcs = ["test_friends/TestFriendsTest.kt"],
+    test_class = "test.TestFriendsTest",
+    deps = [
+        "//third_party/jvm/junit:junit"
+    ],
+    friends = [":test_friends_library"]
+)
+
 filegroup(
     name="cases",
     srcs = [
@@ -115,3 +131,4 @@
     ],
     visibility=["//tests/integrationtests:__subpackages__"]
 )
+
diff --git a/tests/integrationtests/jvm/basic/test_friends/Service.kt b/tests/integrationtests/jvm/basic/test_friends/Service.kt
new file mode 100644
index 0000000..276c2f0
--- /dev/null
+++ b/tests/integrationtests/jvm/basic/test_friends/Service.kt
@@ -0,0 +1,11 @@
+package test
+
+internal const val DEFAULT_FRIEND = "muchacho"
+
+class Service internal constructor(
+  internal val value: String = "hello world"
+) {
+  internal fun iSayHolla(friend: String) {
+    println("holla $friend")
+  }
+}
\ No newline at end of file
diff --git a/tests/integrationtests/jvm/basic/test_friends/TestFriendsTest.kt b/tests/integrationtests/jvm/basic/test_friends/TestFriendsTest.kt
new file mode 100644
index 0000000..ad25e2c
--- /dev/null
+++ b/tests/integrationtests/jvm/basic/test_friends/TestFriendsTest.kt
@@ -0,0 +1,13 @@
+package test
+
+import org.junit.Test
+
+class TestFriendsTest {
+    val service: Service = Service()
+
+    @Test
+    fun testCanAccessFriendMembers() {
+        println(service.value)
+        println(service.iSayHolla(DEFAULT_FRIEND))
+    }
+}
\ No newline at end of file