Skip to content

Commit

Permalink
Merge pull request #1035 from jqno/kotlin-data-class
Browse files Browse the repository at this point in the history
Improved Kotlin support
  • Loading branch information
jqno authored Jan 9, 2025
2 parents 99247ee + d2cd1cd commit a8a0f03
Show file tree
Hide file tree
Showing 14 changed files with 297 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ equalsverifier-release-verify/src/test/resources
bin
target
.DS_Store
kls_database.db

# IntelliJ files
.idea
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Improved Kotlin support. ([Issue 506](https://github.com/jqno/equalsverifier/issues/506#issuecomment-2563664670))

## [3.18] - 2024-12-24

### Added
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Here's a description of the modules:
| equalsverifier-release-main | release assembly for jar with dependencies |
| equalsverifier-release-nodep | release assembly for fat jar (with dependencies shaded in) |
| equalsverifier-release-verify | validation tests for the releases |
| equalsverifier-test-kotlin | tests for Kotlin classes |

## Signed JAR

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
public class FieldInspector<T> {

private final Class<T> type;
private final boolean isKotlin;

public FieldInspector(Class<T> type) {
public FieldInspector(Class<T> type, boolean isKotlin) {
this.type = type;
this.isKotlin = isKotlin;
}

public void check(FieldCheck<T> check) {
for (FieldProbe fieldProbe : FieldIterable.of(type)) {
FieldIterable it = isKotlin ? FieldIterable.ofKotlin(type) : FieldIterable.of(type);
for (FieldProbe fieldProbe : it) {
check.execute(fieldProbe);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public FieldsChecker(Context<T> context) {

@Override
public void check() {
FieldInspector<T> inspector = new FieldInspector<>(context.getType());
FieldInspector<T> inspector = new FieldInspector<>(context.getType(), config.isKotlin());

if (!context.getClassProbe().isEqualsInheritedFromObject()) {
inspector.check(arrayFieldCheck);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public void check() {
return;
}

FieldInspector<T> inspector = new FieldInspector<>(context.getType());
FieldInspector<T> inspector = new FieldInspector<>(context.getType(), context.getConfiguration().isKotlin());
inspector.check(new NullPointerExceptionFieldCheck<>(context));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package nl.jqno.equalsverifier.internal.reflection;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;

/**
* Iterable to iterate over all declared fields in a class and, if needed, over all declared fields of its superclasses.
Expand All @@ -13,12 +12,14 @@ public final class FieldIterable implements Iterable<FieldProbe> {
private final Class<?> type;
private final boolean includeSuperclasses;
private final boolean includeStatic;
private final boolean isKotlin;

/** Private constructor. Call {@link #of(Class)} or {@link #ofIgnoringSuper(Class)} instead. */
private FieldIterable(Class<?> type, boolean includeSuperclasses, boolean includeStatic) {
private FieldIterable(Class<?> type, boolean includeSuperclasses, boolean includeStatic, boolean isKotlin) {
this.type = type;
this.includeSuperclasses = includeSuperclasses;
this.includeStatic = includeStatic;
this.isKotlin = isKotlin;
}

/**
Expand All @@ -29,7 +30,18 @@ private FieldIterable(Class<?> type, boolean includeSuperclasses, boolean includ
* @return A FieldIterable.
*/
public static FieldIterable of(Class<?> type) {
return new FieldIterable(type, true, true);
return new FieldIterable(type, true, true, false);
}

/**
* Factory method for a FieldIterable that iterates over all declared fields of {@code type} and over the declared
* fields of all of its superclasses, but that ignores overridden Kotlin backing fields in superclasses.
*
* @param type The class that contains the fields over which to iterate.
* @return A FieldIterable.
*/
public static FieldIterable ofKotlin(Class<?> type) {
return new FieldIterable(type, true, true, true);
}

/**
Expand All @@ -40,7 +52,7 @@ public static FieldIterable of(Class<?> type) {
* @return A FieldIterable.
*/
public static FieldIterable ofIgnoringSuper(Class<?> type) {
return new FieldIterable(type, false, true);
return new FieldIterable(type, false, true, false);
}

/**
Expand All @@ -51,7 +63,7 @@ public static FieldIterable ofIgnoringSuper(Class<?> type) {
* @return A FieldIterable.
*/
public static FieldIterable ofIgnoringStatic(Class<?> type) {
return new FieldIterable(type, true, false);
return new FieldIterable(type, true, false, false);
}

/**
Expand All @@ -62,7 +74,7 @@ public static FieldIterable ofIgnoringStatic(Class<?> type) {
* @return A FieldIterable.
*/
public static FieldIterable ofIgnoringSuperAndStatic(Class<?> type) {
return new FieldIterable(type, false, false);
return new FieldIterable(type, false, false, false);
}

/**
Expand All @@ -76,6 +88,15 @@ public Iterator<FieldProbe> iterator() {
}

private List<FieldProbe> createFieldList() {
if (isKotlin) {
return createKotlinFieldList();
}
else {
return createJavaFieldList();
}
}

private List<FieldProbe> createJavaFieldList() {
List<FieldProbe> result = new ArrayList<>();

result.addAll(addFieldsFor(type));
Expand All @@ -89,6 +110,24 @@ private List<FieldProbe> createFieldList() {
return result;
}

private List<FieldProbe> createKotlinFieldList() {
List<FieldProbe> result = new ArrayList<>();

result.addAll(addFieldsFor(type));
Set<String> names = result.stream().map(FieldProbe::getName).collect(Collectors.toSet());

if (includeSuperclasses) {
for (Class<?> c : SuperclassIterable.of(type)) {
List<FieldProbe> superFields =
addFieldsFor(c).stream().filter(p -> !names.contains(p.getName())).collect(Collectors.toList());
result.addAll(superFields);
superFields.stream().map(FieldProbe::getName).forEach(names::add);
}
}

return result;
}

private List<FieldProbe> addFieldsFor(Class<?> c) {
List<FieldProbe> fields = new ArrayList<>();
List<FieldProbe> statics = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@ public boolean validate(
Set<String> ignoredAnnotations) {
return "LAZY".equals(properties.getEnumValue("fetch"));
}
};
},

KOTLIN(false, "kotlin.Metadata");

private final boolean inherits;
private final Set<String> partialClassNames;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public final class Configuration<T> {
private final boolean usingGetClass;
private final EnumSet<Warning> warningsToSuppress;
private final Function<String, String> fieldnameToGetter;
private final boolean isKotlin;

private final TypeTag typeTag;
private final AnnotationCache annotationCache;
Expand All @@ -46,6 +47,7 @@ private Configuration(
boolean usingGetClass,
EnumSet<Warning> warningsToSuppress,
Function<String, String> fieldnameToGetter,
boolean isKotlin,
List<T> equalExamples,
List<T> unequalExamples) {
this.type = type;
Expand All @@ -60,6 +62,7 @@ private Configuration(
this.usingGetClass = usingGetClass;
this.warningsToSuppress = warningsToSuppress;
this.fieldnameToGetter = fieldnameToGetter;
this.isKotlin = isKotlin;
this.equalExamples = equalExamples;
this.unequalExamples = unequalExamples;
}
Expand Down Expand Up @@ -91,6 +94,7 @@ public static <T> Configuration<T> build(
actualFields);
Function<String, String> converter =
fieldnameToGetter != null ? fieldnameToGetter : DEFAULT_FIELDNAME_TO_GETTER_CONVERTER;
boolean isKotlin = annotationCache.hasClassAnnotation(type, SupportedAnnotations.KOTLIN);

return new Configuration<>(type,
typeTag,
Expand All @@ -104,6 +108,7 @@ public static <T> Configuration<T> build(
usingGetClass,
warningsToSuppress,
converter,
isKotlin,
equalExamples,
unequalExamples);
}
Expand Down Expand Up @@ -200,6 +205,10 @@ public Function<String, String> getFieldnameToGetter() {
return fieldnameToGetter;
}

public boolean isKotlin() {
return isKotlin;
}

public List<T> getEqualExamples() {
return Collections.unmodifiableList(equalExamples);
}
Expand Down
115 changes: 115 additions & 0 deletions equalsverifier-test-kotlin/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier-parent</artifactId>
<version>3.18.1-SNAPSHOT</version>
</parent>
<packaging>jar</packaging>

<artifactId>equalsverifier-test-kotlin</artifactId>
<name>EqualsVerifier | test Kotlin</name>

<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${version.kotlin}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/main/kotlin</source>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>src/test/kotlin</source>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<jvmTarget>1.8</jvmTarget>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>nl.jqno.equalsverifier</groupId>
<artifactId>equalsverifier-core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${version.junit-jupiter}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${version.assertj}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${version.kotlin}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${version.kotlin}</version>
<scope>test</scope>
</dependency>

</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package nl.jqno.equalsverifier.kotlin

import nl.jqno.equalsverifier.EqualsVerifier
import nl.jqno.equalsverifier.internal.reflection.annotations.AnnotationCache
import nl.jqno.equalsverifier.internal.reflection.annotations.AnnotationCacheBuilder
import nl.jqno.equalsverifier.internal.reflection.annotations.SupportedAnnotations
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

class KotlinCompilerGeneratedAnnotationTest {

val cache = AnnotationCache()
val cacheBuilder = AnnotationCacheBuilder(SupportedAnnotations.values(), HashSet())

@Test
fun `Kotlin classes are recognised as such`() {
assertThat(checkAnnotationFor(KotlinClass::class.java)).isTrue()
}

@Test
fun `Java classes are not recognised as Kotlin classes`() {
assertThat(checkAnnotationFor(EqualsVerifier::class.java)).isFalse()
}

private fun checkAnnotationFor(type: Class<*>): Boolean {
cacheBuilder.build(type, cache)
return cache.hasClassAnnotation(type, SupportedAnnotations.KOTLIN)
}
}

class KotlinClass
Loading

0 comments on commit a8a0f03

Please sign in to comment.