Internal defaulted constructor parameter makes the dependency `api` if another parameter is a value class
Build scan link https://gradle.com/s/jcmca6qg7cvfs
Plugin version 2.10.1
Gradle version 8.13
JDK version 21
(Optional) Kotlin and Kotlin Gradle Plugin (KGP) version 2.1.10
(Optional) reason output for bugs relating to incorrect advice
------------------------------------------------------------
You asked about the dependency 'org.slf4j:slf4j-api:1.7.10'.
You have been advised to change this dependency to 'api' from 'implementation'.
------------------------------------------------------------
Shortest path from root project to org.slf4j:slf4j-api:1.7.10 for compileClasspath:
:
\--- org.slf4j:slf4j-api:1.7.10
Shortest path from root project to org.slf4j:slf4j-api:1.7.10 for runtimeClasspath:
:
\--- org.slf4j:slf4j-api:1.7.10
Shortest path from root project to org.slf4j:slf4j-api:1.7.10 for testCompileClasspath:
:
\--- org.slf4j:slf4j-api:1.7.10
Shortest path from root project to org.slf4j:slf4j-api:1.7.10 for testRuntimeClasspath:
:
\--- org.slf4j:slf4j-api:1.7.10
Source: main
------------
* Exposes 1 class: org.slf4j.Logger (implies api).
Source: test
------------
(no usages)
Describe the bug
A dependency exposed in an internal constructor as a defaulted parameter is incorrectly seen as part of the module's public API, if another dependency is an @JvmInline value class.
To Reproduce
Steps to reproduce the behaviour:
git clone [email protected]:Mahoney-bug-examples/build-health-reproducer.gitcd build-health-reproducer./gradlew buildHealth
Expected behavior
The build should pass.
Additional context
In the reproducer project, change dependency: Dependency to dependency: String in the internal constructor of Service and you will see that the plugin correctly considers slf4j to be an implementation dependency.
FWIW, decompiling the byte code when the other parameter is a value class gives this:
@Metadata(
mv = {2, 1, 0},
k = 1,
xi = 48,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0019\b\u0000\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\b\b\u0002\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006¨\u0006\u0007"},
d2 = {"Lfoo/Service;", "", "dependency", "Lfoo/Dependency;", "logger", "Lorg/slf4j/Logger;", "(Ljava/lang/String;Lorg/slf4j/Logger;Lkotlin/jvm/internal/DefaultConstructorMarker;)V", "build-health-reproducer"}
)
public final class Service {
private Service(String dependency, Logger logger) {
Intrinsics.checkNotNullParameter(dependency, "dependency");
Intrinsics.checkNotNullParameter(logger, "logger");
super();
}
// $FF: synthetic method
public Service(String var1, Logger var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 2) != 0) {
Logger var5 = LoggerFactory.getLogger(Service.class);
Intrinsics.checkNotNullExpressionValue(var5, "getLogger(...)");
var2 = var5;
}
this(var1, var2, (DefaultConstructorMarker)null);
}
// $FF: synthetic method
public Service(String dependency, Logger logger, DefaultConstructorMarker $constructor_marker) {
this(dependency, logger);
}
}
Whereas when it's a String and the bug goes away, it looks like this:
@Metadata(
mv = {2, 1, 0},
k = 1,
xi = 48,
d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0010\u000e\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\u0018\u00002\u00020\u0001B\u0019\b\u0000\u0012\u0006\u0010\u0002\u001a\u00020\u0003\u0012\b\b\u0002\u0010\u0004\u001a\u00020\u0005¢\u0006\u0002\u0010\u0006¨\u0006\u0007"},
d2 = {"Lfoo/Service;", "", "dependency", "", "logger", "Lorg/slf4j/Logger;", "(Ljava/lang/String;Lorg/slf4j/Logger;)V", "build-health-reproducer"}
)
public final class Service {
public Service(@NotNull String dependency, @NotNull Logger logger) {
Intrinsics.checkNotNullParameter(dependency, "dependency");
Intrinsics.checkNotNullParameter(logger, "logger");
super();
}
// $FF: synthetic method
public Service(String var1, Logger var2, int var3, DefaultConstructorMarker var4) {
if ((var3 & 2) != 0) {
Logger var5 = LoggerFactory.getLogger(Service.class);
Intrinsics.checkNotNullExpressionValue(var5, "getLogger(...)");
var2 = var5;
}
this(var1, var2);
}
}
I presume the differences explain the different ways the plugin interprets the dependency visibility.
Thanks for the report. I suspect this relates to https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1172.