Skip to content

Commit

Permalink
feat!: Immutable Collections Extension added
Browse files Browse the repository at this point in the history
closes #106

BREAKING CHANGE: Extensions API changed
  • Loading branch information
pawellabaj committed Jul 24, 2023
1 parent 92e8de4 commit cb1fdae
Show file tree
Hide file tree
Showing 43 changed files with 1,558 additions and 37 deletions.
1 change: 1 addition & 0 deletions .github/semantic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
titleAndCommits: true
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ It provides an easy way to avoid writing repetitive boilerplate code. It generat
* [builders](https://github.com/pawellabaj/auto-record/wiki/Record-Builder) - incorporating [Randgalt/record-builder](https://github.com/Randgalt/record-builder) library
* [memoization](https://github.com/pawellabaj/auto-record/wiki/Memoization)
* [ignoring fields](https://github.com/pawellabaj/auto-record/wiki/Ignored-components) in `hashCode()` and `equals()` methods
* generated _common_ methods if the record has an [array component](https://github.com/pawellabaj/auto-record/wiki/Array-components)
* generated _common_ methods if the record has an [array recordComponent](https://github.com/pawellabaj/auto-record/wiki/Array-components)
* exclusion from [JaCoCo test coverage](https://github.com/pawellabaj/auto-record/wiki/JaCoCo-exclusion) analysis

AutoRecord allows users to customize record generation process by:
Expand Down
92 changes: 92 additions & 0 deletions extensions/immutable-collections/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?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>pl.com.labaj</groupId>
<artifactId>auto-record-project</artifactId>
<!-- <version>2.1.0</version>-->
<version>2.1.1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

<artifactId>auto-record-immutable-collections</artifactId>
<version>1.0.0-SNAPSHOT</version>

<properties>
<guava.version>32.1.1-jre</guava.version>

<license-maven-plugin.header>${project.basedir}/../../.build/lic-header.txt</license-maven-plugin.header>
</properties>

<dependencies>
<dependency>
<groupId>pl.com.labaj</groupId>
<artifactId>auto-record</artifactId>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>pl.com.labaj</groupId>
<artifactId>auto-record</artifactId>
<version>${project.parent.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<configuration>
<licenseSets>
<licenseSet>
<header>${license-maven-plugin.header}</header>
</licenseSet>
</licenseSets>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>verify</id>

<properties>
<sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/../../target/site/jacoco-aggregate/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package pl.com.labaj.autorecord.extension.compact;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeVariableName;
import pl.com.labaj.autorecord.context.Logger;
import pl.com.labaj.autorecord.context.StaticImports;

import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import java.util.Map;

import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.STATIC;
import static pl.com.labaj.autorecord.extension.compact.ImmutableCollectionsExtension.ENUM_SET;
import static pl.com.labaj.autorecord.extension.compact.ImmutableCollectionsExtension.GUAVA_IMMUTABLE_SORTED_SET_CLASS_NAME;
import static pl.com.labaj.autorecord.extension.compact.ImmutableCollectionsExtension.SET;
import static pl.com.labaj.autorecord.extension.compact.ImmutableCollectionsExtension.SORTED_SET;

class AdditionalMethodBuilder {
private static final Map<String, MethodBuilder> METHOD_BUILDERS = Map.of(
SET, builderForSet()
);

private final Types typeUtils;

private final Map<String, TypeMirror> immutableTypes;
private final Map<String, TypeMirror> collectionTypes;
private final StaticImports staticImports;
private final Logger logger;

AdditionalMethodBuilder(Types typeUtils,
Map<String, TypeMirror> immutableTypes,
Map<String, TypeMirror> collectionTypes,
StaticImports staticImports,
Logger logger) {
this.typeUtils = typeUtils;
this.immutableTypes = immutableTypes;
this.collectionTypes = collectionTypes;
this.staticImports = staticImports;
this.logger = logger;
}

MethodSpec buildMethodFor(String mark) {
return METHOD_BUILDERS.get(mark).buildMethod(typeUtils, immutableTypes, collectionTypes, staticImports, logger);
}

private static MethodBuilder builderForSet() {
return (typeUtils, immutableTypes, collectionTypes, staticImports, logger) -> {
var typeVariableName = TypeVariableName.get("E");
var setType = collectionTypes.get(SET);
var className = ClassName.get((TypeElement) typeUtils.asElement(setType));
var setTypeName = ParameterizedTypeName.get(className, typeVariableName);
var parameterSpec = ParameterSpec.builder(setTypeName, "set").build();
var methodBuilder = MethodSpec.methodBuilder("_immutableSet")
.addModifiers(PRIVATE, STATIC)
.addTypeVariable(typeVariableName)
.returns(setTypeName)
.addParameter(parameterSpec);

var enumSetType = immutableTypes.get(ENUM_SET);
immutableTypes.values().stream()
.filter(immutableType -> typeUtils.isSubtype(immutableType, setType))
.forEach(immutableType -> {
var isEnumSet = typeUtils.isSameType(immutableType, enumSetType);
methodBuilder.beginControlFlow("if (set instanceof $T<$L>)", immutableType, isEnumSet ? '?' : 'E')
.addStatement("return set")
.endControlFlow();
});

methodBuilder.beginControlFlow("if (set instanceof $T<E> sortedSet)", collectionTypes.get(SORTED_SET))
.addStatement("return $T.copyOfSorted(sortedSet)", GUAVA_IMMUTABLE_SORTED_SET_CLASS_NAME)
.endControlFlow()
.addStatement("return set");

return methodBuilder.build();
};
}

@FunctionalInterface
private static interface MethodBuilder {
MethodSpec buildMethod(Types typeUtils,
Map<String, TypeMirror> immutableTypes,
Map<String, TypeMirror> collectionTypes,
StaticImports staticImports,
Logger logger);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package pl.com.labaj.autorecord.extension.compact;

/*-
* Copyright © 2023 Auto Record
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import pl.com.labaj.autorecord.context.Context;
import pl.com.labaj.autorecord.context.Logger;
import pl.com.labaj.autorecord.context.StaticImports;
import pl.com.labaj.autorecord.extension.CompactConstructorExtension;

import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import static java.util.Objects.nonNull;
import static java.util.stream.Collectors.toMap;
import static javax.lang.model.type.TypeKind.ARRAY;
import static pl.com.labaj.autorecord.extension.compact.RecordComponent.getComponentsDebugInfo;

public class ImmutableCollectionsExtension implements CompactConstructorExtension {

static final ClassName GUAVA_IMMUTABLE_SORTED_SET_CLASS_NAME
= ClassName.get("com.google.common.collect", "ImmutableSortedSet");

static final String NAVIGABLE_SET = "java.util.NavigableSet";
static final String SORTED_SET = "java.util.SortedSet";
static final String SET = "java.util.Set";

static final String ENUM_SET = "java.util.EnumSet";
private static final String GUAVA_IMMUTABLE_SET = "com.google.common.collect.ImmutableSet";
private static final Set<String> COLLECTION_NAMES = Set.of(
NAVIGABLE_SET,
SORTED_SET,
SET
);

private Types typeUtils;
private Logger logger;
private final Set<String> knownImmutableTypeNames;
private Map<String, TypeMirror> collectionTypes;
private List<RecordComponent> componentsToProcess;
private Set<String> additionalMethodMarks;
private Map<String, TypeMirror> immutableTypes;

public ImmutableCollectionsExtension() {
knownImmutableTypeNames = new HashSet<>();
knownImmutableTypeNames.add(ENUM_SET);
knownImmutableTypeNames.add(GUAVA_IMMUTABLE_SET);
}

@Override
public void init(String[] parameters) {
knownImmutableTypeNames.addAll(Arrays.asList(parameters));
}

@Override
public boolean shouldGenerateCompactConstructor(boolean isGeneratedByProcessor, Context context) {
typeUtils = context.processingEnv().getTypeUtils();
logger = context.logger();

var elementUtils = context.processingEnv().getElementUtils();

record NameType(String name, TypeMirror type) {}
immutableTypes = knownImmutableTypeNames.stream()
.map(name -> {
var typeElement = elementUtils.getTypeElement(name);
return nonNull(typeElement) ? new NameType(name, typeUtils.erasure(typeElement.asType())) : null;
})
.filter(Objects::nonNull)
.collect(toMap(
NameType::name,
NameType::type
));
collectionTypes = COLLECTION_NAMES.stream()
.map(name -> {
var typeElement = elementUtils.getTypeElement(name);
return nonNull(typeElement) ? new NameType(name, typeUtils.erasure(typeElement.asType())) : null;
})
.filter(Objects::nonNull)
.collect(toMap(
NameType::name,
NameType::type
));

var componentBuilder = new RecordComponent.Builder(typeUtils, immutableTypes, collectionTypes, logger);
var recordComponents = context.components()
.stream()
.filter(recordComponent -> !recordComponent.type().getKind().isPrimitive())
.filter(recordComponent -> recordComponent.type().getKind() != ARRAY)
.map(componentBuilder::toExtensionRecordComponent)
.toList();
if (logger.isDebugEnabled()) {
logger.debug("Record components:\n" + getComponentsDebugInfo(recordComponents));
}

if (recordComponents.isEmpty()) {
return false;
}

componentsToProcess = recordComponents.stream()
.filter(RecordComponent::shouldBeProcessed)
.toList();

return !componentsToProcess.isEmpty();
}

@Override
public CodeBlock suffixCompactConstructorContent(Context context, StaticImports staticImports) {
if (true) { //TODO: debugger info
logger.note("Components to process:\n" + getComponentsDebugInfo(componentsToProcess));
}

var codeBuilder = CodeBlock.builder();

var statementBuilder = new StatementBuilder(typeUtils, collectionTypes, staticImports, logger);

componentsToProcess.stream()
.map(statementBuilder::statementFor)
.filter(Objects::nonNull)
.forEach(codeBuilder::addStatement);

additionalMethodMarks = statementBuilder.additionalMethods();

return codeBuilder.build();
}

@Override
public List<MethodSpec> additionalMethodsToSupportCompactConstructor(Context context, StaticImports staticImports) {
if (additionalMethodMarks.isEmpty()) {
return List.of();
}

var additionalMethodsBuilder = new AdditionalMethodBuilder(typeUtils, immutableTypes, collectionTypes, staticImports, logger);
return additionalMethodMarks.stream()
.map(additionalMethodsBuilder::buildMethodFor)
.toList();
}
}
Loading

0 comments on commit cb1fdae

Please sign in to comment.