Git Product home page Git Product logo

binary-compatibility-validator's Introduction

Kotlin Alpha JetBrains official project Maven Central License KDoc link

Binary compatibility validator

The tool allows dumping binary API of a JVM part of a Kotlin library that is public in the sense of Kotlin visibilities and ensures that the public binary API wasn't changed in a way that makes this change binary incompatible.

Contents

Requirements

Binary compatibility validator plugin requires Gradle 6.1.1 or newer.

Kotlin version 1.6.20 or newer.

Setup

Binary compatibility validator is a Gradle plugin that can be added to your build in the following way:

  • in build.gradle.kts
plugins {
    id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.16.3"
}
  • in build.gradle
plugins {
    id 'org.jetbrains.kotlinx.binary-compatibility-validator' version '0.16.3'
}

It is enough to apply the plugin only to the root project build file; all sub-projects will be configured automatically.

Tasks

The plugin provides two tasks:

  • apiDump — builds the project and dumps its public API in project api subfolder. API is dumped in a human-readable format. If API dump already exists, it will be overwritten.
  • apiCheck — builds the project and checks that project's public API is the same as golden value in project api subfolder. This task is automatically inserted into check pipeline, so both build and check tasks will start checking public API upon their execution.

For projects with multiple JVM targets, multiple subfolders will be created, e.g. api/jvm and api/android

Optional parameters

Binary compatibility validator can be additionally configured with the following DSL:

Groovy

apiValidation {
    /**
     * Packages that are excluded from public API dumps even if they
     * contain public API. 
     */
    ignoredPackages += ["kotlinx.coroutines.internal"]

    /**
     * Sub-projects that are excluded from API validation 
     */
    ignoredProjects += ["benchmarks", "examples"]

    /**
     * Classes (fully qualified) that are excluded from public API dumps even if they
     * contain public API.
     */
    ignoredClasses += ["com.company.BuildConfig"]

    /**
     * Set of annotations that exclude API from being public.
     * Typically, it is all kinds of `@InternalApi` annotations that mark 
     * effectively private API that cannot be actually private for technical reasons.
     */
    nonPublicMarkers += ["my.package.MyInternalApiAnnotation"]

    /**
     * Flag to programmatically disable compatibility validator
     */
    validationDisabled = true

    /**
     * A path to a subdirectory inside the project root directory where dumps should be stored.
     */
    apiDumpDirectory = "api"
}

Kotlin

apiValidation {
    /**
     * Packages that are excluded from public API dumps even if they
     * contain public API.
     */
    ignoredPackages.add("kotlinx.coroutines.internal")

    /**
     * Sub-projects that are excluded from API validation
     */
    ignoredProjects.addAll(listOf("benchmarks", "examples"))

    /**
     * Classes (fully qualified) that are excluded from public API dumps even if they
     * contain public API.
     */
    ignoredClasses.add("com.company.BuildConfig")
    
    /**
     * Set of annotations that exclude API from being public.
     * Typically, it is all kinds of `@InternalApi` annotations that mark
     * effectively private API that cannot be actually private for technical reasons.
     */
    nonPublicMarkers.add("my.package.MyInternalApiAnnotation")

    /**
     * Flag to programmatically disable compatibility validator
     */
    validationDisabled = false

    /**
     * A path to a subdirectory inside the project root directory where dumps should be stored.
     */
    apiDumpDirectory = "aux/validation"
}

Producing dump of a jar

By default, binary compatibility validator analyzes project output class files from build/classes directory when building an API dump. If you pack these classes into an output jar not in a regular way, for example, by excluding certain classes, applying shadow plugin, and so on, the API dump built from the original class files may no longer reflect the resulting jar contents accurately. In that case, it makes sense to use the resulting jar as an input of the apiBuild task:

Kotlin

tasks {
    apiBuild {
        // "jar" here is the name of the default Jar task producing the resulting jar file
        // in a multiplatform project it can be named "jvmJar"
        // if you applied the shadow plugin, it creates the "shadowJar" task that produces the transformed jar
        inputJar.value(jar.flatMap { it.archiveFile })
    }
}

Workflow

When starting to validate your library public API, we recommend the following workflow:

  • Preparation phase (one-time action):

    • As the first step, apply the plugin, configure it and execute apiDump.
    • Validate your public API manually.
    • Commit .api files to your VCS.
    • At this moment, default check task will validate public API along with test run and will fail the build if API differs.
  • Regular workflow

    • When doing code changes that do not imply any changes in public API, no additional actions should be performed. check task on your CI will validate everything.
    • When doing code changes that imply changes in public API, whether it is a new API or adjustments in existing one, check task will start to fail. apiDump should be executed manually, the resulting diff in .api file should be verified: only signatures you expected to change should be changed.
    • Commit the resulting .api diff along with code changes.

Experimental KLib ABI validation support

The KLib validation support is experimental and is a subject to change (applies to both an API and the ABI dump format). A project has to use Kotlin 1.9.20 or newer to use this feature.

To validate public ABI of a Kotlin library (KLib) corresponding option should be enabled explicitly:

apiValidation {
    @OptIn(kotlinx.validation.ExperimentalBCVApi::class)
    klib {
        enabled = true
    }
}

When enabled, KLib support adds additional dependencies to existing apiDump and apiCheck tasks. Generate KLib ABI dumps are places alongside JVM dumps (in api subfolder, by default) in files named <project name>.klib.api. The dump file combines all dumps generated for individual targets with declarations specific to some targets being annotated with corresponding target names. During the validation phase, that file is compared to the dump extracted from the latest version of the library, and any differences between these two files are reported as errors.

Currently, all options described in Optional parameters section are supported for klibs too. The only caveat here is that all class names should be specified in the JVM-format, like package.name.ClassName$SubclassName.

Please refer to a design document for details on the format and rationale behind the current implementation.

KLib ABI dump generation and validation on Linux and Windows hosts

Currently, compilation to Apple-specific targets (like iosArm64 or watchosX86) supported only on Apple hosts. To ease the development on Windows and Linux hosts, binary compatibility validator does not validate ABI for targets not supported on the current host, even if .klib.api file contains declarations for these targets.

This behavior could be altered to force an error when klibs for some targets could not be compiled:

apiValidation {
    @OptIn(kotlinx.validation.ExperimentalBCVApi::class)
    klib {
        enabled = true
        // treat a target being unsupported on a host as an error
        strictValidation = true
    }
}

When it comes to dump generation (apiDump task) on non-Apple hosts, binary compatibility validator attempts to infer an ABI from dumps generated for supported targets and an old dump from project's api folder (if any). Inferred dump may not match an actual dump, and it is recommended to update a dump on hosts supporting all required targets, if possible.

What constitutes the public API

Classes

A class is considered to be effectively public if all the following conditions are met:

  • it has public or protected JVM access (ACC_PUBLIC or ACC_PROTECTED)
  • it has one of the following visibilities in Kotlin:
    • no visibility (means no Kotlin declaration corresponds to this compiled class)
    • public
    • protected
    • internal, only in case if the class is annotated with PublishedApi
  • it isn't a local class
  • it isn't a synthetic class with mappings for when tableswitches ($WhenMappings)
  • it contains at least one effectively public member, in case if the class corresponds to a kotlin file with top-level members or a multifile facade
  • in case if the class is a member in another class, it is contained in the effectively public class
  • in case if the class is a protected member in another class, it is contained in the non-final class

Members

A member of the class (i.e. a field or a method) is considered to be effectively public if all the following conditions are met:

  • it has public or protected JVM access (ACC_PUBLIC or ACC_PROTECTED)

  • it has one of the following visibilities in Kotlin:

    • no visibility (means no Kotlin declaration corresponds to this class member)
    • public
    • protected
    • internal, only in case if the class is annotated with PublishedApi

    Note that Kotlin visibility of a field exposed by lateinit property is the visibility of its setter.

  • in case if the member is protected, it is contained in non-final class

  • it isn't a synthetic access method for a private field

What makes an incompatible change to the public binary API

Class changes

For a class a binary incompatible change is:

  • changing the full class name (including package and containing classes)
  • changing the superclass, so that the class no longer has the previous superclass in the inheritance chain
  • changing the set of implemented interfaces so that the class no longer implements interfaces it had implemented before
  • changing one of the following access flags:
    • ACC_PUBLIC, ACC_PROTECTED, ACC_PRIVATE — lessening the class visibility
    • ACC_FINAL — making non-final class final
    • ACC_ABSTRACT — making non-abstract class abstract
    • ACC_INTERFACE — changing class to interface and vice versa
    • ACC_ANNOTATION — changing annotation to interface and vice versa

Class member changes

For a class member a binary incompatible change is:

  • changing its name
  • changing its descriptor (erased return type and parameter types for methods); this includes changing field to method and vice versa
  • changing one of the following access flags:
    • ACC_PUBLIC, ACC_PROTECTED, ACC_PRIVATE — lessening the member visibility
    • ACC_FINAL — making non-final field or method final
    • ACC_ABSTRACT — making non-abstract method abstract
    • ACC_STATIC — changing instance member to static and vice versa

Building the project locally

In order to build and run tests in the project in IDE, two prerequisites are required:

  • Java 11 or above in order to use the latest ASM
  • All build actions in the IDE should be delegated to Gradle

Contributing

Read the Contributing Guidelines.

binary-compatibility-validator's People

Contributors

3flex avatar alikhachev avatar anoop44 avatar benedekh avatar bnorm avatar bryanlogan avatar chao2zhang avatar cortinico avatar dnpetrov avatar etolstoy avatar fzhinkin avatar goooler avatar ilya-g avatar joffrey-bion avatar kirpichenkovpavel avatar ligee avatar martinbonnin avatar movshin avatar pdvrieze avatar qwwdfsad avatar sandwwraith avatar sellmair avatar sergejisbrecht avatar shanshin avatar twyatt avatar udalov avatar xtrm-en avatar ychescale9 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

binary-compatibility-validator's Issues

introduce ignoredClasses in gradle plugin

Type: Feat. request

Issue:
Android-Projects create a BuildConfig for android-libraries, which is public. It is not possible to exclude the package, because other needed classes are in given package. Also it is not possible to use an annotation, because it is generated.

Suggestion:
Introduce new ignoredClasses flag in gradle

Code

ignoredClasses += ["com.my.package.MyClazz"]

Note:
When this feat. is approved, I would implement it quickly and create a PR.

Ignore synthetic annotation classes produced by directly instantiated annotations

In Kotlin 1.6.0, synthetic annotation classes are present in the API dump and I think they should be automatically excluded.

I can manually ignore them, but it's tedious and prone to breaking due to the mangled names.

Example from here: slackhq/EitherNet#29

public synthetic class com/slack/eithernet/AnnotationsKt$annotationImpl$com_slack_eithernet_StatusCode$0 : com/slack/eithernet/StatusCode {
	public fun <init> (I)V
	public final synthetic fun annotationType ()Ljava/lang/Class;
	public final fun equals (Ljava/lang/Object;)Z
	public final fun hashCode ()I
	public final fun toString ()Ljava/lang/String;
	public final synthetic fun value ()I
}

With kotlin 1.5.0: internal and inline definitions are included in api dump

Kotlin version: 1.5.0
BCV version: 0.2.3

It seems that when this tool is used with Kotlin 1.5.0, inline functions and internal classes and functions are included in the api dumps. This was not happening before, and seems incorrect - internal things aren't part of a library's public api, that's the whole point of internal.

See this PR for an example: square/workflow-kotlin#417 - the PR simply updates the kotlin version (and some core kotlin libraries), and there's a huge number of lines of diff for the .api files.

I just realized that project is still using a much older version of this tool, updating first to see if that fixes it…

Java Android Library Support

Binary compatibility plugin configures necessary tasks like 'apiDump' and 'apiCheck' only if kotlin, kotlin-android or kotlin-multiplatform plugins are applied as can be seen here

Although it can be used for Java libraries, due to missing the plugins listed above, necessary tasks are not created.

Java Android libraries can be supported if the tasks are also created for projects where com.android.library plugin is applied.

Allow applying the plugin to a subproject

At the moment, the plugin must be applied to the root project. Otherwise, you get Extension of type 'ApiValidationExtension' does not exist error during the root project configuration. In my case, there is a Gradle multi-project with a hierarchy of hundreds of sub-projects and I want to validate only a single one. It seems to be quite natural to apply the plugin only to this single subproject.

As a workaround, I can apply the plugin to the root project and add everything to the ignoredProjects except the needed project. It works but doesn't look nice.

Companion object 'const val' properties generate false positive if they have a non-public marker

The following:

@Target(AnnotationTarget.PROPERTY)
annotation class HiddenProperty

public class Foo {
    companion object {
        @HiddenProperty
        const val bar = "barValue"
    }
}

Generates the following pseudo Java code:

public final class Foo {
   @NotNull
   public static final String bar = "barValue";
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      // $FF: synthetic method
      @HiddenProperty
      public static void getBar$annotations() {
      }

      private Companion() {
      }
   }
}

Because the synthetic method carrying the annotations is not in the same class as the field, it is ignored and the field is marked as public API even though it shouldn't.

Is it possible to distinguish between compatible and incompatible changes?

When monitoring public API, I can think of two use cases:

  1. Make sure that module.api files are always up-to-date and represent the latest state of the public API.
  2. Make sure that a new version of module.api is backward compatible with the previous one to avoid breaking changes.

In the case of 1., it's important to detect additions as we want them to be taken into account in subsequents checks.

But additions are typically not important for 2.: It is fine to release a new version of a lib with new APIs as it will not break backward compatibility.

My current understanding is that ./gradlew apiCheck is focused at 1. and will fail on any change, even additions. Am I correct?

If that's the case, it would be nice to have another task focused at 2. that will only succeed if the new API is backward compatible.

Maybe ./gradlew apiDumpSafe ?

It would fail on backward incompatible changes only and output a report with the problematic changes and whether they are source-level or binary-level incompatibilities.

Is that currently possible? Or will it be?

Non-public markers aren't handled correctly for properties

When declaring an opt-in annotation and adding that annotation to the nonPublicMarkers configuration, properties that have the annotation present are not correctly excluded from the api dump.

Example annotation:

@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
annotation class MyInternalApi

A function like this will not show up in the dump (correctly):

@MyInternalApi
fun internalFun() {}

However, this property will:

@MyInternalApi
var internalVar = 0

... in the form of a getter and a setter:

public static final fun getInternalVar ()I
public static final fun setInternalVar (I)V

A workaround that removes these from the API dump is annotating the getter and setter directly, like so:

var otherInternalVar = 0
    @MyInternalApi get
    @MyInternalApi set

However, annotating the getter like that doesn't trigger warnings at the call site of the property, so you really need three annotations on each property to satisfy both the plugin and inspections:

@MyInternalApi
var combinedInternalVar = 0
    @MyInternalApi get
    @MyInternalApi set

Project with examples and reproducing the issue can be found here.

Execution failed for task 'apiBuild'.

Execution failed for task ':mirai-core-qqandroid:apiBuild'.
> Key net/mamoe/mirai/qqandroid/network/protocol/data/proto/SubMsgType0xc1 is missing in the map.

To reproduce:

  1. Check out https://github.com/mamoe/mirai
  2. Run task apiDump

I have these classes in the same package

internal class SubMsgType0xc1 

internal class Submsgtype0xc1 {
    internal class Submsgtype0xc1 : ProtoBuf {
         // ...
    }
}

In a multi module Gradle project, ignore root project by default

Observed Behavior

In a multi-module Gradle project, after running apiDump, the file api/<projectName>.api will appear with a blank line.

Current workaround

I need to specifically ignore the root projects to avoid the creation of this file. This looks quite unnecessary:

For example,

apiValidation {
    ignoredProjects.addAll(subprojects.filter { it.name != "detekt-api"}.map { it.name } + rootProject.name)
}

Environment

The project uses a buildSrc and all scripts are in kts.

Handle TypeErasure

public suspend fun foo(): String
// change to 
public suspend fun foo(): Int

The api is always public final fun foo (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; and the apiCheck will not fail.

private constructor shows up in API dump

The following code:

class FooBar private constructor(val id: String) {
  class Builder {
    fun build() = FooBar("")
  }
}

dumps the following API:

public final class com/apollographql/apollo3/api/FooBar {
	public synthetic fun <init> (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
	public final fun getId ()Ljava/lang/String;
}

public final class com/apollographql/apollo3/api/FooBar$Builder {
	public fun <init> ()V
	public final fun build ()Lcom/apollographql/apollo3/api/FooBar;
}

I'm curious as to why <init> is listed in the dump. If I were to remove the id parameter, would that be considered a breaking change even if it should be an implementation detail?

I'm using 0.8.0-RC

Support Android projects

Currently, the plugin will ignore any subprojects that are built with the Android Gradle Plugin.

Top-level projects don't seem to work on all OS's

I'm not entirely sure if I'm reading this all right, but in our single-project repo doesn't appear to work when running apiCheck on linux CI while it does work on macOS locally.

Here's a repro - slackhq/EitherNet#52. Fails on CI with a strange check that suggests it's expecting a subproject, but it's a single-project repo only.

Plugin fails with DuplicateTaskException when used on Multiplatform projects with both JVM and Android targets

I have a Kotlin Multiplatform project that defines both jvm and android targets (this is a snippet of the gradle config, I can include the full one if it's helpful):

plugins {
    kotlin("multiplatform")
    id("com.android.library")
    id("binary-compatibility-validator")
}

kotlin {
    android {
        publishLibraryVariants("release")
    }
    jvm()
    ...
}

If I apply v0.4.0 of the binary-compatibility-validator gradle plugin, I get the following error:

FAILURE: Build failed with an exception.

* What went wrong:
A problem occurred configuring project ':runtime'.
> org.gradle.api.internal.tasks.DefaultTaskContainer$DuplicateTaskException: Cannot add task 'apiBuild' as a task with that name already exists.

This error didn't happen with v0.2.4 of the plugin. Looking at https://github.com/Kotlin/binary-compatibility-validator/blob/master/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt#L85, it seems that the plugin tries to create a task named apiDump for every jvm or android compilation in the project. This works fine if the project is configured with a jvm target or an android target, but not if the project is configured with both targets.

Related to #38

Support API comparison with published artifact

Hi! Thanks for publishing this as separate plugin!

In our team, we use binary-compatibility-validator from Kotlin repository for internal shared libraries but suggested workflow (committing .api files to repository) is too strict for us, so instead we implemented comparison with the latest published maven artifact ${project.group}:${project.name}:latest.release.

At this time we just post an API diff to PRs, but we also have a plans to check only for a binary-incompatible changes by comparing ClassBinarySignature/MemberBinarySignature contents.

It would be great if similar functionality available in this plugin.

Should override functions be included in the API spec?

Adding or removing an override function is binary compatible in Java so why are these functions included in the public API spec? I suppose they are technically public or protected but it does add some extra work when making binary compatible changes.

Allow ignoring project by path

I have a project where multiple modules have the same name but different path (:feature1:sample, :feature2:sample) and currently I cannot ignore just one of them since you specify the name of the module which in both cases is sample.

This could be done in a backwards compatible way by checking if the ignored project starts with : and if so project.path is used instead.

Unable to apply to ktor

Cannot add task 'apiBuild' as a task with that name already exists.

Steps:

  1. Checkout ktor master
  2. Apply the following patch
Index: build.gradle
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- build.gradle	(revision e16afa03df5721646c7daa4233fe980c6c1ca391)
+++ build.gradle	(date 1583323513139)
@@ -54,6 +54,7 @@
         classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
         classpath "me.champeau.gradle:jmh-gradle-plugin:$jmh_plugin_version"
         classpath "org.jetbrains.kotlinx:kotlinx.benchmark.gradle:$benchmarks_version"
+        classpath "org.jetbrains.kotlinx:binary-compatibility-validator:$validator_version"
     }
 }
 
@@ -95,6 +96,8 @@
     }
 }
 
+apply plugin: 'binary-compatibility-validator'
+
 allprojects {
     group = "io.ktor"
     version = configuredVersion
Index: gradle.properties
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- gradle.properties	(revision e16afa03df5721646c7daa4233fe980c6c1ca391)
+++ gradle.properties	(date 1583323144984)
@@ -22,6 +22,7 @@
 serialization_version=0.14.0
 coroutines_version=1.3.3
 atomicfu_version=0.14.1
+validator_version=0.2.1
 
 # server
 netty_version=4.1.44.Final

Build fails if api is not dumped

The build fails with non-informative error (directory not found) if the initial API dump is not done before the build.
I think that the default behavior should be to give a warning and ignore the plugin altogether.

Non-public markers aren't handled correctly for functions with default parameters

When declaring an opt-in annotation and adding that annotation to the nonPublicMarkers configuration, functions that have default parameters are not correctly excluded from the api dump - the generated $default function is still included. A very similar issue to the one fixed in #30.

Example annotation:

@RequiresOptIn(level = RequiresOptIn.Level.WARNING)
annotation class MyInternalApi

An example function:

@MyInternalApi
fun defParamInternalFun(x: Int = 0) {}

The incorrect API dump:

public final class co/zsmb/example/MainKt {
	public static synthetic fun defParamInternalFun$default (IILjava/lang/Object;)V
}

Project with examples and reproducing the issue can be found here.

Is `runtime` vs `compile` time scope in a POM considered?

Our use case is that we have a multi-module project that we ship as an SDK - so many modules in it are effectively public - but our "clients" consume from only a specific module point - so many of those public classes in upstream projects are not on the compile (in the .pom) scope, but in the runtime scope.

We make breaking changes to our runtime API frequently, but can we use this tool to limit changes to our compile API as our SDK is consumed from the point of view of a specific module?

Example:

:a - consumed by client
:b - consumed by :a as an implementation dependency
:c - consumed by :a as an implementation dependency
:d - consumed by :a as an api dependency

We want to be notified of breaking public API changes to :a and :d, but not :b and :c

Nested classes is present in dump when outer class is annotated with nonPublicMarker

apiValidation {
    nonPublicMarkers += ["com.example.InternalApi"]
}
package com.example

@InternalApi
class Example {
    class Nested {
        fun foo() {}
    }
}

produces following dump:

public final class com/example/Example$Nested {
	public fun <init> ()V
	public final fun foo ()V
}

In Kotlin, an opt-in annotations is propagated to an all class members, including a nested classes.
But binary-compatibility-validator filters only classes that are directly annotated with one of the annotations from nonPublicMarkers.

I think this behaviour should be aligned with opt-in annotations as they are often used as nonPublicMarkers.

Internal class ComposeSingletons is presented in the api dump

I'm using kotlin 1.5.31 with compose 1.0.5.

My api dump contains a lot of following classes

public final class cloud/jablotron/apps/lib/auth/oauth2/ui/screens/login/ComposableSingletons$LoginScreenKt {
	public static final field INSTANCE Lcloud/jablotron/apps/lib/auth/oauth2/ui/screens/login/ComposableSingletons$LoginScreenKt;
	public static field lambda-1 Lkotlin/jvm/functions/Function3;
	public static field lambda-2 Lkotlin/jvm/functions/Function2;
	public fun <init> ()V
	public final fun getLambda-1$library_release ()Lkotlin/jvm/functions/Function3;
	public final fun getLambda-2$library_release ()Lkotlin/jvm/functions/Function2;
}

I'm not sure whether the issue is in this library or in the compose.

After brief look at the compose code, I assume that these classes should be internal so they shouldn't be presented in the api dump. Since compose code is not generated and is not possible to take a look at the code. It is really hard to get rid of these. Do you think that the issue is in the library? Or do you have an idea how to get rid of it?
https://github.com/androidx/androidx/blob/4356507bc51920e48d01612f4e6f69f907055456/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/LambdaMemoizationRegressionTests.kt

Thanks

Add support of semantic minor change

I am building SDKs and follow semantic versioning. It would be nice to have an option to detect the minor API update during apiCheck command. At this moment the added class would fail the apiCheck command.

But from SDK consumer point of view this change is compatible with the previous version because it does not require any effort in order to update to it.

Feature request:

  • add an option to mark minor change as a non-breaking change. apiCheck should succeed
  • print the added API to the console in this case

module-info.java in compilation source causes IllegalStateException

Reproducer: Kotlin/kotlinx.serialization#1624
Exception:

Caused by: java.lang.IllegalStateException: superName must not be null
        at kotlinx.validation.api.KotlinSignaturesLoadingKt$loadApiFromJvmClasses$4.invoke(KotlinSignaturesLoading.kt:56)
        at kotlinx.validation.api.KotlinSignaturesLoadingKt$loadApiFromJvmClasses$4.invoke(KotlinSignaturesLoading.kt)
        at kotlin.sequences.TransformingSequence$iterator$1.next(Sequences.kt:172)
        at kotlin.collections.CollectionsKt___CollectionsKt.toCollection(_Collections.kt:1206)
        at kotlin.collections.CollectionsKt___CollectionsKt.toMutableList(_Collections.kt:1239)
        at kotlin.collections.CollectionsKt___CollectionsKt.sortedWith(_Collections.kt:988)
        at kotlinx.validation.api.KotlinSignaturesLoadingKt.loadApiFromJvmClasses(KotlinSignaturesLoading.kt:162)
        at kotlinx.validation.api.KotlinSignaturesLoadingKt.loadApiFromJvmClasses$default(KotlinSignaturesLoading.kt:20)
        at kotlinx.validation.KotlinApiBuildTask.generate(KotlinApiBuildTask.kt:49)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:104)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:58)

Cause:
https://github.com/Kotlin/binary-compatibility-validator/blob/master/src/main/kotlin/api/KotlinSignaturesLoading.kt#L41
superName is null for module-info class. ASM and validator do not expect that.
Possible solutions: make superName nullable or just filter out such classes.

Note that visibilityFilter is actually not used anywhere, filtering by fq name (ignoredClasses) happens after loading ASM.

Inconsistent Gradle behavior across different Java versions

I have a simple setup in this PR: ZacSweers/redacted-compiler-plugin#39

apply plugin: "binary-compatibility-validator"

apiValidation {
    ignoredProjects += ["sample", "sample-android"]
}

With JDK 8, the sample-android project is seen just fine. With JDK 11+, it fails to find it for some reason.

* What went wrong:
A problem occurred configuring root project 'redacted-compiler-plugin'.
> Cannot find excluded project sample-android in all projects: [redacted-compiler-plugin, redacted-compiler-plugin, redacted-compiler-plugin-annotations, sample]

Add an option to disable `allprojects` behavior.

allprojects is considered a bad practice in Gradle and it will break future features like project isolation.

Should be great if one of these options was available before the final release:

  • Add a flag to disable allprojects configuration
  • Remove allprojects configuration and force it to apply to each project individually. To avoid duplicating code, the recommended practice nowadays is using convention plugins, so a simple example can be added to the readme.

`apiDump` task is always in `SKIPPED` state when plugin applied on Android project.

This check is always false https://github.com/Kotlin/binary-compatibility-validator/blob/master/src/main/kotlin/BinaryCompatibilityValidatorPlugin.kt#L87 for Android project.
This is because source files are located in src/main/java or src/main/kotlin by convention, but sourceSets list points only to src/release/kotlin and src/release/java.
There's no KotlinJvmAndroidCompilation with name main.

If I remove compilation.allKotlinSourceSets.any { it.kotlin.srcDirs.any { it.exists() } } check, API dump is generated.

Plugin marker is missing on Gradle Portal

A follow up from #44

I suppose the plugin is now published on Gradle Portal 🎉

However, it looks like the publishing is missing the Gradle marker. The only way to use this plugin with the plugins{} block that you mention in the README is by providing a rule in the resolutionStrategy on the settings.gradle.kts file:

pluginManagement {
    resolutionStrategy {
        eachPlugin {
            if (requested.id.id == "binary-compatibility-validator") {
                useModule("org.jetbrains.kotlinx:binary-compatibility-validator:${requested.version}")
            }
        }
    }
}

You can find a real world example here: https://github.com/cortinico/ktfmt-gradle/blob/b6f8a55bfd8334dccacb7f28335176534cb8ee27/plugin-build/settings.gradle.kts#L2-L8

I'm not entirely sure what is happening as it seems like the plugin is actually on the Gradle portal here: https://plugins.gradle.org/m2/org/jetbrains/kotlinx/binary-compatibility-validator/0.4.0/

While the WebUI will show just a blank page, the artifacts will still resolve correctly.

Can I ask you to clarify how you published the plugin on Gradle Portal? Also having a badge on the readme would be useful :)

Is an added 'synthetic' considered to be a compatible change?

(This is not a bug, just a question)
To one of my functions, a synthetic was added in the .api file.

Before:
public final fun myFunction (Ljava/util/List;)I
After:
public final synthetic fun myFunction (Ljava/util/List;)I

Would this be considered to be a non-breaking change?

Support ignoring groups of projects

For example, using wildcards. I have a library with a samples/ directory that contains a bunch of subprojects that aren't published to Maven and don't have any public API, and it would be nice to be able to exclude everything in that directory with a single configuration.

Support MPP with multiple JVM targets

    jvm("androidJvm")
    jvm("desktopJvm")
Cannot add task 'apiBuild' as a task with that name already exists.

Using 0.3.0. Maybe we can create tasks based on the target names, e.g. androidJvmApiBuild, desktopJvmApiBuild

Prevent `apiCheck` task fail until tests are finished.

We often run CI integration tests without running apiDump. The apiCheck task can fail the build without running tests or showing the results.

Please consider delaying running(or failing) apiCheck task until tests are finished so we can observe test results.

After upgrading to kotlin 1.5.0, the kotlin-parcelize generated code started to be reported as public by binary-compatibility-validator

Hi,

I'm using kotlin-parcelize plugin to generate the parcelable code in the internal data classes in my project.

Let's consider the following kotlin data class, that is marked as internal.

@Parcelize
internal data class Country (
    val code: String,
    val name: String
) : Parcelable

I have made the following tests with binary-compatibility-validator 0.5.0.
And there is a difference when using kotlin 1.5.0 and 1.4.32.

Before upgrading to kotlin 1.5.0, using kotlin 1.4.32, the command ./gradlew teleconsultationandroid:apiDump wouldn't return any output related to the class Country.
And to help debug this issue, here is the compiled code of Country in kotlin 1.4.32.

Compiled `Country` data class using kotlin 1.4.32. import android.os.Parcel; import android.os.Parcelable; import kotlin.Metadata; import kotlin.jvm.internal.Intrinsics; import kotlinx.parcelize.Parcelize; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;

@parcelize
@metadata(...)
/* compiled from: Country.kt */
public final class Country implements Parcelable {
public static final Parcelable.Creator CREATOR = new Creator();
@NotNull
private final String code;
@NotNull
private final String name;

@Metadata(bv = {1, 0, 3}, d1 = {}, d2 = {}, k = 3, mv = {1, 4, 2})
public static class Creator implements Parcelable.Creator<Country> {
    @Override // android.os.Parcelable.Creator
    @NotNull
    public final Country createFromParcel(@NotNull Parcel parcel) {
        Intrinsics.checkNotNullParameter(parcel, "in");
        return new Country(parcel.readString(), parcel.readString());
    }

    @Override // android.os.Parcelable.Creator
    @NotNull
    public final Country[] newArray(int i) {
        return new Country[i];
    }
}

public Country(@NotNull String str, @NotNull String str2) {
    Intrinsics.checkNotNullParameter(str, "code");
    Intrinsics.checkNotNullParameter(str2, "name");
    this.code = str;
    this.name = str2;
}

public static /* synthetic */ Country copy$default(Country country, String str, String str2, int i, Object obj) {
    if ((i & 1) != 0) {
        str = country.code;
    }
    if ((i & 2) != 0) {
        str2 = country.name;
    }
    return country.copy(str, str2);
}

@NotNull
public final String component1() {
    return this.code;
}

@NotNull
public final String component2() {
    return this.name;
}

@NotNull
public final Country copy(@NotNull String str, @NotNull String str2) {
    Intrinsics.checkNotNullParameter(str, "code");
    Intrinsics.checkNotNullParameter(str2, "name");
    return new Country(str, str2);
}

public int describeContents() {
    return 0;
}

public boolean equals(@Nullable Object obj) {
    if (this != obj) {
        if (obj instanceof Country) {
            Country country = (Country) obj;
            if (!Intrinsics.areEqual(this.code, country.code) || !Intrinsics.areEqual(this.name, country.name)) {
                return false;
            }
        }
        return false;
    }
    return true;
}

@NotNull
public final String getCode() {
    return this.code;
}

@NotNull
public final String getName() {
    return this.name;
}

public int hashCode() {
    int i = 0;
    String str = this.code;
    int hashCode = (str != null ? str.hashCode() : 0) * 31;
    String str2 = this.name;
    if (str2 != null) {
        i = str2.hashCode();
    }
    return hashCode + i;
}

@NotNull
public String toString() {
    return "Country(code=" + this.code + ", name=" + this.name + ")";
}

public void writeToParcel(@NotNull Parcel parcel, int i) {
    Intrinsics.checkNotNullParameter(parcel, "parcel");
    parcel.writeString(this.code);
    parcel.writeString(this.name);
}

}

But after upgrading to kotlin 1.5.0, the command ./gradlew teleconsultationandroid:apiDump started returing the following output.

public final class com/sample/project/Country$Creator : android/os/Parcelable$Creator {
	public fun <init> ()V
	public synthetic fun createFromParcel (Landroid/os/Parcel;)Ljava/lang/Object;
	public final fun createFromParcel (Landroid/os/Parcel;)L com/sample/project/Country;
	public synthetic fun newArray (I)[Ljava/lang/Object;
	public final fun newArray (I)[Lcom/sample/project/Country;
}

And here is the output of the compiled code in kotlin 1.5.0.

Compiled `Country` data class using kotlin 1.5.0. import android.os.Parcel; import android.os.Parcelable; import kotlin.Metadata; import kotlin.jvm.internal.Intrinsics; import kotlinx.parcelize.Parcelize; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable;

@parcelize
@metadata(...)
/* compiled from: Country.kt */
public final class Country implements Parcelable {
@NotNull
public static final Parcelable.Creator CREATOR = new Creator();
@NotNull
private final String code;
@NotNull
private final String name;

@Metadata(bv = {1, 0, 3}, d1 = {}, d2 = {}, k = 3, mv = {1, 5, 1})
/* compiled from: Country.kt */
public static final class Creator implements Parcelable.Creator<Country> {
    @Override // android.os.Parcelable.Creator
    @NotNull
    public final Country createFromParcel(@NotNull Parcel parcel) {
        Intrinsics.checkNotNullParameter(parcel, "parcel");
        return new Country(parcel.readString(), parcel.readString());
    }

    @Override // android.os.Parcelable.Creator
    @NotNull
    public final Country[] newArray(int i) {
        return new Country[i];
    }
}

public Country(@NotNull String str, @NotNull String str2) {
    Intrinsics.checkNotNullParameter(str, "code");
    Intrinsics.checkNotNullParameter(str2, "name");
    this.code = str;
    this.name = str2;
}

public static /* synthetic */ Country copy$default(Country country, String str, String str2, int i, Object obj) {
    if ((i & 1) != 0) {
        str = country.code;
    }
    if ((i & 2) != 0) {
        str2 = country.name;
    }
    return country.copy(str, str2);
}

@NotNull
public final String component1() {
    return this.code;
}

@NotNull
public final String component2() {
    return this.name;
}

@NotNull
public final Country copy(@NotNull String str, @NotNull String str2) {
    Intrinsics.checkNotNullParameter(str, "code");
    Intrinsics.checkNotNullParameter(str2, "name");
    return new Country(str, str2);
}

public int describeContents() {
    return 0;
}

public boolean equals(@Nullable Object obj) {
    if (this == obj) {
        return true;
    }
    if (!(obj instanceof Country)) {
        return false;
    }
    Country country = (Country) obj;
    if (!Intrinsics.areEqual(this.code, country.code)) {
        return false;
    }
    return Intrinsics.areEqual(this.name, country.name);
}

@NotNull
public final String getCode() {
    return this.code;
}

@NotNull
public final String getName() {
    return this.name;
}

public int hashCode() {
    return (this.code.hashCode() * 31) + this.name.hashCode();
}

@NotNull
public String toString() {
    return "Country(code=" + this.code + ", name=" + this.name + ')';
}

public void writeToParcel(@NotNull Parcel parcel, int i) {
    Intrinsics.checkNotNullParameter(parcel, "out");
    parcel.writeString(this.code);
    parcel.writeString(this.name);
}

}

Is this as issue of binary-compatibility-validator?
Thanks

Support public external JS declarations

Currently, the validator does not create .api files or api declarations for public external declarations, which results into not checking the JS declarations, which could be called from library consumer in Kotlin.

apiDump task incompatible with configuration cache

FAILURE: Build failed with an exception.

* What went wrong:
Configuration cache problems found in this build.

10 problems were found storing the configuration cache.
- Task `:detekt-api:apiDump` of type `org.gradle.api.tasks.Sync`: cannot serialize object of type 'kotlinx.validation.KotlinApiBuildTask', a subtype of 'org.gradle.api.Task', as these are not supported with the configuration cache.
  See https://docs.gradle.org/7.5/userguide/configuration_cache.html#config_cache:requirements:task_access
- Task `:detekt-api:apiDump` of type `org.gradle.api.tasks.Sync`: cannot serialize object of type 'org.gradle.api.internal.project.DefaultProject', a subtype of 'org.gradle.api.Project', as these are not supported with the configuration cache.
  See https://docs.gradle.org/7.5/userguide/configuration_cache.html#config_cache:requirements:disallowed_types

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.