Skip to content

Commit 0803b50

Browse files
authored
Merge pull request #14 from pettermahlen/synthetic-constructors
Avoid synthetic constructors by default
2 parents ff2aafa + 08f8ab8 commit 0803b50

27 files changed

Lines changed: 706 additions & 44 deletions

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,18 @@ if (message.isLogin()) {
252252
`Second_dataenum`, it must of course be public.
253253

254254

255+
## Configuration
256+
257+
DataEnum currently has a single configurable setting determining the visibility of constructors in
258+
generated code. Generally speaking, `private` is best as it ensures there is a single way of creating
259+
case instances (the generated static factory methods like `MyMessages.login(String, String)` above).
260+
However, for Android development, you want to keep the method count down to a minimum, and private
261+
constructors lead to synthetic constructors being generated, increasing the method count. Since that
262+
is an important use case for us, we've chosen the package-private as the default. This is configurable
263+
through adding a
264+
[`@ConstructorAccess`](https://javadoc.io/page/com.spotify.dataenum/dataenum/latest/com/spotify/dataenum/ConstructorAccess.html)
265+
annotation to a `package-info.java` file. See the javadocs for more information.
266+
255267
## Known weaknesses of DataEnum
256268

257269
- While the generated classes are immutable, they do not enforce that parameters are immutable. It is up to users of
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* -\-\-
3+
* DataEnum
4+
* --
5+
* Copyright (c) 2017 Spotify AB
6+
* --
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* -/-/-
19+
*/
20+
package com.spotify.dataenum.processor;
21+
22+
import com.spotify.dataenum.Access;
23+
import com.spotify.dataenum.ConstructorAccess;
24+
import java.util.ArrayList;
25+
import java.util.List;
26+
import java.util.Optional;
27+
import java.util.Set;
28+
import javax.lang.model.element.Element;
29+
import javax.lang.model.element.Modifier;
30+
import javax.lang.model.element.PackageElement;
31+
32+
/**
33+
* Determines the most appropriate access level based on package-level annotations. Uses a
34+
* simplistic algorithm that should perform well given a small number of annotated packages, which
35+
* seems like a reasonable assumption to make.
36+
*/
37+
class AccessSelector {
38+
39+
// sorted so that the longest names are first; this means the first hit is going to be the best
40+
// one
41+
private final List<PackageAndAccess> packages;
42+
43+
AccessSelector(Set<? extends Element> visibilityAnnotatedPackages) {
44+
packages = parseAnnotatedPackages(visibilityAnnotatedPackages);
45+
}
46+
47+
private List<PackageAndAccess> parseAnnotatedPackages(
48+
Set<? extends Element> visibilityAnnotatedPackages) {
49+
ArrayList<PackageAndAccess> result = new ArrayList<>(visibilityAnnotatedPackages.size());
50+
51+
for (Element element : visibilityAnnotatedPackages) {
52+
if (!(element instanceof PackageElement)) {
53+
throw new IllegalArgumentException(
54+
"received a access annotated element that is not a package: " + element);
55+
}
56+
57+
PackageElement packageElement = (PackageElement) element;
58+
59+
result.add(
60+
new PackageAndAccess(
61+
packageElement.getQualifiedName().toString(),
62+
element.getAnnotation(ConstructorAccess.class).value()));
63+
}
64+
65+
result.sort((o1, o2) -> o2.packageName.length() - o1.packageName.length());
66+
67+
return result;
68+
}
69+
70+
public Optional<Modifier> accessModifierFor(String packageName) {
71+
switch (accessFor(packageName)) {
72+
case PACKAGE_PRIVATE:
73+
return Optional.empty();
74+
case PRIVATE:
75+
return Optional.of(Modifier.PRIVATE);
76+
case PROTECTED:
77+
return Optional.of(Modifier.PROTECTED);
78+
case PUBLIC:
79+
return Optional.of(Modifier.PUBLIC);
80+
}
81+
82+
throw new AssertionError("cannot get here");
83+
}
84+
85+
private Access accessFor(String packageName) {
86+
for (PackageAndAccess packageAndAccess : packages) {
87+
if (isParentOf(packageAndAccess.packageName, packageName)) {
88+
return packageAndAccess.access;
89+
}
90+
}
91+
92+
return Access.PACKAGE_PRIVATE;
93+
}
94+
95+
private boolean isParentOf(String maybeParentPackage, String packageName) {
96+
// check the easy case first
97+
if (!packageName.startsWith(maybeParentPackage)) {
98+
return false;
99+
}
100+
101+
// even if they start with the same string, there might be a mismatch in the last package name
102+
// in the possible parent. For instance, "com.spot" is not a parent package of "com.spotify".
103+
String[] parentParts = maybeParentPackage.split("\\.");
104+
String[] packageParts = packageName.split("\\.");
105+
106+
String lastParentPackagePart = parentParts[parentParts.length - 1];
107+
108+
return packageParts[parentParts.length - 1].equals(lastParentPackagePart);
109+
}
110+
111+
private static class PackageAndAccess {
112+
113+
private final String packageName;
114+
private final Access access;
115+
116+
private PackageAndAccess(String packageName, Access access) {
117+
this.packageName = packageName;
118+
this.access = access;
119+
}
120+
}
121+
}

dataenum-processor/src/main/java/com/spotify/dataenum/processor/DataEnumProcessor.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
*/
2020
package com.spotify.dataenum.processor;
2121

22+
import com.spotify.dataenum.ConstructorAccess;
2223
import com.spotify.dataenum.DataEnum;
2324
import com.spotify.dataenum.DataenumUtils;
2425
import com.spotify.dataenum.processor.data.OutputSpec;
@@ -50,6 +51,9 @@ public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv
5051
Filer filer = processingEnv.getFiler();
5152
Messager messager = processingEnv.getMessager();
5253

54+
AccessSelector accessSelector =
55+
new AccessSelector(roundEnvironment.getElementsAnnotatedWith(ConstructorAccess.class));
56+
5357
for (Element element : roundEnvironment.getElementsAnnotatedWith(DataEnum.class)) {
5458
try {
5559

@@ -59,7 +63,10 @@ public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv
5963
}
6064

6165
OutputSpec outputSpec = OutputSpecFactory.create(spec);
62-
TypeSpec outputTypeSpec = SpecTypeFactory.create(outputSpec);
66+
TypeSpec outputTypeSpec =
67+
SpecTypeFactory.create(
68+
outputSpec,
69+
accessSelector.accessModifierFor(outputSpec.outputClass().packageName()));
6370

6471
JavaFile.Builder javaFileBuilder =
6572
JavaFile.builder(outputSpec.outputClass().packageName(), outputTypeSpec);

dataenum-processor/src/main/java/com/spotify/dataenum/processor/generator/spec/SpecTypeFactory.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*/
2020
package com.spotify.dataenum.processor.generator.spec;
2121

22+
import static com.spotify.dataenum.processor.util.Iterables.fromOptional;
23+
2224
import com.spotify.dataenum.processor.DataEnumProcessor;
2325
import com.spotify.dataenum.processor.data.OutputSpec;
2426
import com.spotify.dataenum.processor.data.OutputValue;
@@ -34,14 +36,16 @@
3436
import com.squareup.javapoet.TypeSpec;
3537
import java.util.ArrayList;
3638
import java.util.List;
39+
import java.util.Optional;
3740
import javax.annotation.Generated;
3841
import javax.lang.model.element.Modifier;
3942

4043
public final class SpecTypeFactory {
4144

4245
private SpecTypeFactory() {}
4346

44-
public static TypeSpec create(OutputSpec spec) throws ParserException {
47+
public static TypeSpec create(OutputSpec spec, Optional<Modifier> constructorAccessModifier)
48+
throws ParserException {
4549
List<TypeSpec> valueTypes = new ArrayList<>();
4650
List<MethodSpec> factoryMethods = new ArrayList<>();
4751
List<MethodSpec> isMethods = new ArrayList<>();
@@ -51,7 +55,9 @@ public static TypeSpec create(OutputSpec spec) throws ParserException {
5155
MapMethods mapMethods = new MapMethods(spec.outputValues());
5256

5357
for (OutputValue value : spec.outputValues()) {
54-
valueTypes.add(ValueTypeFactory.create(value, spec, matchMethods, mapMethods));
58+
valueTypes.add(
59+
ValueTypeFactory.create(
60+
value, spec, matchMethods, mapMethods, constructorAccessModifier));
5561

5662
ValueMethods valueMethods = new ValueMethods(value);
5763
factoryMethods.add(valueMethods.createFactoryMethod(spec));
@@ -69,8 +75,11 @@ public static TypeSpec create(OutputSpec spec) throws ParserException {
6975
.addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT)
7076
.addTypeVariables(spec.typeVariables());
7177

72-
// Hide the constructor
73-
enumBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build());
78+
// add constructor with correct access
79+
enumBuilder.addMethod(
80+
MethodSpec.constructorBuilder()
81+
.addModifiers(fromOptional(constructorAccessModifier))
82+
.build());
7483

7584
enumBuilder.addTypes(valueTypes);
7685
enumBuilder.addMethods(factoryMethods);

dataenum-processor/src/main/java/com/spotify/dataenum/processor/generator/value/ValueTypeFactory.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*/
2020
package com.spotify.dataenum.processor.generator.value;
2121

22+
import static com.spotify.dataenum.processor.util.Iterables.fromOptional;
23+
2224
import com.spotify.dataenum.processor.data.OutputSpec;
2325
import com.spotify.dataenum.processor.data.OutputValue;
2426
import com.spotify.dataenum.processor.data.Parameter;
@@ -38,6 +40,7 @@
3840
import java.util.ArrayList;
3941
import java.util.Arrays;
4042
import java.util.List;
43+
import java.util.Optional;
4144
import javax.annotation.Nonnull;
4245
import javax.annotation.Nullable;
4346
import javax.lang.model.element.Modifier;
@@ -50,7 +53,11 @@ public class ValueTypeFactory {
5053
private ValueTypeFactory() {}
5154

5255
public static TypeSpec create(
53-
OutputValue value, OutputSpec spec, MatchMethods matchMethods, MapMethods mapMethods)
56+
OutputValue value,
57+
OutputSpec spec,
58+
MatchMethods matchMethods,
59+
MapMethods mapMethods,
60+
Optional<Modifier> constructorAccessModifier)
5461
throws ParserException {
5562

5663
TypeSpec.Builder typeBuilder =
@@ -59,7 +66,7 @@ public static TypeSpec create(
5966
.superclass(getSuperclassForValue(value, spec))
6067
.addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL);
6168

62-
typeBuilder.addMethod(createConstructor(value));
69+
typeBuilder.addMethod(createConstructor(value, constructorAccessModifier));
6370
typeBuilder.addFields(createFields(value));
6471
typeBuilder.addMethods(createGetters(value));
6572
typeBuilder.addMethod(createEquals(value));
@@ -102,8 +109,10 @@ private static TypeName getSuperclassForValue(OutputValue value, OutputSpec spec
102109
spec.outputClass(), superParameters.toArray(new TypeName[] {}));
103110
}
104111

105-
private static MethodSpec createConstructor(OutputValue value) {
106-
MethodSpec.Builder constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE);
112+
private static MethodSpec createConstructor(
113+
OutputValue value, Optional<Modifier> constructorAccessModifier) {
114+
MethodSpec.Builder constructor =
115+
MethodSpec.constructorBuilder().addModifiers(fromOptional(constructorAccessModifier));
107116
for (Parameter parameter : value.parameters()) {
108117
constructor.addParameter(parameter.type(), parameter.name());
109118

dataenum-processor/src/main/java/com/spotify/dataenum/processor/parser/ValuesParser.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ static List<Value> parse(TypeElement enumElement, ProcessingEnvironment processi
4848
.getMessager()
4949
.printMessage(
5050
Kind.ERROR,
51-
"Duplicate case name '" + value.name().toLowerCase() + "' - lower-case case names must be unique.",
51+
"Duplicate case name '"
52+
+ value.name().toLowerCase()
53+
+ "' - lower-case case names must be unique.",
5254
valueElement);
5355
}
5456

dataenum-processor/src/main/java/com/spotify/dataenum/processor/util/Iterables.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121

2222
import java.lang.reflect.Array;
2323
import java.util.ArrayList;
24+
import java.util.Collections;
2425
import java.util.List;
26+
import java.util.Optional;
2527

2628
public class Iterables {
2729
public static <T> boolean contains(Iterable<T> iterable, T value) {
@@ -53,4 +55,8 @@ public static <T> int sizeOf(Iterable<T> iterable) {
5355

5456
return size;
5557
}
58+
59+
public static <T> Iterable<T> fromOptional(Optional<T> input) {
60+
return input.map(Collections::singletonList).orElse(Collections.emptyList());
61+
}
5662
}

0 commit comments

Comments
 (0)