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 25, 2023
1 parent 92e8de4 commit fef91fa
Show file tree
Hide file tree
Showing 57 changed files with 2,183 additions and 112 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,220 @@
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 com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
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.util.Types;
import java.util.ArrayDeque;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;

import static java.util.Comparator.reverseOrder;
import static java.util.Objects.isNull;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.collectingAndThen;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
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.PROCESSED_TYPES;
import static pl.com.labaj.autorecord.extension.compact.ProcessedType.allProcessedTypes;

class AdditionalMethodsGenerator {
private static final EnumMap<ProcessedType, MethodGenerator> METHOD_GENERATORS = allProcessedTypes().stream()
.collect(toMap(
identity(),
AdditionalMethodsGenerator::builderFor,
(b1, b2) -> b1,
() -> new EnumMap<>(ProcessedType.class)
));

private final Types typeUtils;

private final StaticImports staticImports;
private final Logger logger;

AdditionalMethodsGenerator(Types typeUtils,
StaticImports staticImports,
Logger logger) {
this.typeUtils = typeUtils;
this.staticImports = staticImports;
this.logger = logger;
}

List<MethodSpec> generateAdditionalMethods(List<RecordComponent> componentsToProcess, ProcessedTypesStructure structure) {
var pTypesToGenerate = collectPTypesToGenerate(componentsToProcess, structure);

return ProcessedType.allProcessedTypes().stream()
.filter(pTypesToGenerate::contains)
.sorted(reverseOrder())
.map(METHOD_GENERATORS::get)
.map(methodGenerator -> methodGenerator.generateMethod(typeUtils, structure, staticImports, logger))
.toList();
}

private HashSet<ProcessedType> collectPTypesToGenerate(List<RecordComponent> componentsToProcess, ProcessedTypesStructure structure) {
var pTypesToCheck = componentsToProcess.stream()
.map(RecordComponent::pType)
.collect(collectingAndThen(toSet(), ArrayDeque::new));

var pTypesToGenerate = new HashSet<ProcessedType>();

while (!pTypesToCheck.isEmpty()) {
var pType = pTypesToCheck.removeFirst();
if (structure.needsAdditionalMethod(pType)) {
pTypesToGenerate.add(pType);
}

pType.directSubTypes().stream()
.filter(subType -> !pTypesToCheck.contains(subType) && !pTypesToGenerate.contains(subType))
.forEach(pTypesToCheck::addLast);
}

return pTypesToGenerate;
}

private static MethodGenerator builderFor(ProcessedType pType) {
return (typeUtils, structure, staticImports, logger) -> {
var methodBuilder = getMethodBuilder(typeUtils, pType);

immutableTypesBlock(structure, pType)
.ifPresent(methodBuilder::addCode);
subTypesBlocks(pType, structure)
.forEach(methodBuilder::addCode);

var returnStatement = isNull(pType.factoryClassName())
? CodeBlock.of("return $L", pType.argumentName())
: CodeBlock.of("return $T.$L($L)", pType.factoryClassName(), pType.factoryMethodName(), pType.argumentName());
methodBuilder.addStatement(returnStatement);

return methodBuilder.build();
};
}

private static MethodSpec.Builder getMethodBuilder(Types typeUtils, ProcessedType pType) {
var typeVariableNames = pType.genericNames().stream()
.map(TypeVariableName::get)
.toList();
var typeName = getTypeName(typeUtils, pType, typeVariableNames);
var parameterSpec = ParameterSpec.builder(typeName, pType.argumentName()).build();

var builder = MethodSpec.methodBuilder("_" + pType.factoryMethodName())
.addModifiers(PRIVATE, STATIC)
.returns(typeName)
.addParameter(parameterSpec);

typeVariableNames.forEach(builder::addTypeVariable);

return builder;
}

private static TypeName getTypeName(Types typeUtils, ProcessedType pType, List<TypeVariableName> typeVariableNames) {
var collectionType = PROCESSED_TYPES.get(pType);

if (typeVariableNames.isEmpty()) {
return TypeName.get(collectionType);
}

var className = ClassName.get((TypeElement) typeUtils.asElement(collectionType));

return ParameterizedTypeName.get(className, typeVariableNames.toArray(TypeVariableName[]::new));
}

private static Optional<CodeBlock> immutableTypesBlock(ProcessedTypesStructure structure, ProcessedType pType) {
var immutableTypes = structure.getImmutableTypes(pType);
if (immutableTypes.isEmpty()) {
return Optional.empty();
}

var ifFormat = new StringBuilder("if (");
var i = 0;
for (var iterator = immutableTypes.iterator(); iterator.hasNext(); i++) {
iterator.next();
String name = pType.argumentName();

ifFormat.append(name).append(" instanceof $T");
if (!pType.genericNames().isEmpty()) {
var genericClause = pType.genericNames().stream()
.collect(joining(",", "<", ">"));
ifFormat.append(genericClause);
}

if (iterator.hasNext()) {
ifFormat.append(i == 0 ? "\n$>$>|| " : "\n|| ");
}
}
ifFormat.append(")");

var size = immutableTypes.size();
var block = CodeBlock.builder()
.beginControlFlow(ifFormat.toString(), immutableTypes.toArray())
.addStatement(size > 1 ? "$<$<return $L" : "return $L", pType.argumentName())
.endControlFlow()
.build();

return Optional.of(block);
}

private static List<CodeBlock> subTypesBlocks(ProcessedType pType, ProcessedTypesStructure structure) {
return pType.directSubTypes().stream()
.filter(structure::needsAdditionalMethod)
.sorted(reverseOrder())
.map(subPType -> subTypeBlock(subPType, pType, structure))
.toList();
}

private static CodeBlock subTypeBlock(ProcessedType pType, ProcessedType parent, ProcessedTypesStructure structure) {
var argumentName = pType.argumentName();
var factoryClassName = pType.factoryClassName();
var factoryMethodName = pType.factoryMethodName();

var parentGenericNames = parent.genericNames();
var genericClause = parentGenericNames.isEmpty() || !pType.checkGenericInInstanceOf()
? ""
: parentGenericNames.stream().collect(joining(",", "<", ">"));
var statement = structure.needsAdditionalMethod(pType)
? CodeBlock.of("return _$L($L)", factoryMethodName, argumentName)
: CodeBlock.of("return $T.$L($L)", factoryClassName, factoryMethodName, argumentName);

return CodeBlock.builder()
.beginControlFlow("if ($L instanceof $T$L $L)", parent.argumentName(), PROCESSED_TYPES.get(pType), genericClause, argumentName)
.addStatement(statement)
.endControlFlow()
.build();
}

@FunctionalInterface
private interface MethodGenerator {
MethodSpec generateMethod(Types typeUtils,
ProcessedTypesStructure structure,
StaticImports staticImports,
Logger logger);
}
}
Loading

0 comments on commit fef91fa

Please sign in to comment.