dependency-analysis-gradle-plugin icon indicating copy to clipboard operation
dependency-analysis-gradle-plugin copied to clipboard

Internal defaulted constructor parameter makes the dependency `api` if another parameter is a value class

Open Mahoney opened this issue 10 months ago • 2 comments

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:

  1. git clone [email protected]:Mahoney-bug-examples/build-health-reproducer.git
  2. cd build-health-reproducer
  3. ./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.

Mahoney avatar Mar 06 '25 12:03 Mahoney

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.

Mahoney avatar Mar 06 '25 13:03 Mahoney

Thanks for the report. I suspect this relates to https://github.com/autonomousapps/dependency-analysis-gradle-plugin/issues/1172.

autonomousapps avatar Mar 06 '25 18:03 autonomousapps