lombok.eclipse.handlers.EclipseSingularsRecipes Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2015-2021 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.eclipse.handlers;
import static lombok.eclipse.handlers.EclipseHandlerUtil.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.internal.compiler.ast.ASTNode;
import org.eclipse.jdt.internal.compiler.ast.AbstractMethodDeclaration;
import org.eclipse.jdt.internal.compiler.ast.Annotation;
import org.eclipse.jdt.internal.compiler.ast.Argument;
import org.eclipse.jdt.internal.compiler.ast.Block;
import org.eclipse.jdt.internal.compiler.ast.ConditionalExpression;
import org.eclipse.jdt.internal.compiler.ast.EqualExpression;
import org.eclipse.jdt.internal.compiler.ast.Expression;
import org.eclipse.jdt.internal.compiler.ast.FieldDeclaration;
import org.eclipse.jdt.internal.compiler.ast.FieldReference;
import org.eclipse.jdt.internal.compiler.ast.IfStatement;
import org.eclipse.jdt.internal.compiler.ast.IntLiteral;
import org.eclipse.jdt.internal.compiler.ast.MessageSend;
import org.eclipse.jdt.internal.compiler.ast.NullLiteral;
import org.eclipse.jdt.internal.compiler.ast.OperatorIds;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedQualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.ParameterizedSingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.QualifiedTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Reference;
import org.eclipse.jdt.internal.compiler.ast.ReturnStatement;
import org.eclipse.jdt.internal.compiler.ast.SingleNameReference;
import org.eclipse.jdt.internal.compiler.ast.SingleTypeReference;
import org.eclipse.jdt.internal.compiler.ast.Statement;
import org.eclipse.jdt.internal.compiler.ast.ThisReference;
import org.eclipse.jdt.internal.compiler.ast.TypeReference;
import org.eclipse.jdt.internal.compiler.ast.Wildcard;
import org.eclipse.jdt.internal.compiler.lookup.ClassScope;
import org.eclipse.jdt.internal.compiler.lookup.MethodScope;
import org.eclipse.jdt.internal.compiler.lookup.TypeConstants;
import org.eclipse.jdt.internal.compiler.lookup.TypeIds;
import lombok.AccessLevel;
import lombok.core.LombokImmutableList;
import lombok.core.SpiLoadUtil;
import lombok.core.TypeLibrary;
import lombok.core.configuration.CheckerFrameworkVersion;
import lombok.eclipse.EclipseNode;
import lombok.eclipse.handlers.HandleBuilder.BuilderJob;
public class EclipseSingularsRecipes {
public interface TypeReferenceMaker {
TypeReference make();
}
public interface StatementMaker {
Statement make();
}
private static final EclipseSingularsRecipes INSTANCE = new EclipseSingularsRecipes();
private final Map singularizers = new HashMap();
private final TypeLibrary singularizableTypes = new TypeLibrary();
private EclipseSingularsRecipes() {
try {
loadAll(singularizableTypes, singularizers);
singularizableTypes.lock();
} catch (IOException e) {
System.err.println("Lombok's @Singularizable feature is broken due to misconfigured SPI files: " + e);
}
}
private static void loadAll(TypeLibrary library, Map map) throws IOException {
for (EclipseSingularizer handler : SpiLoadUtil.findServices(EclipseSingularizer.class, EclipseSingularizer.class.getClassLoader())) {
for (String type : handler.getSupportedTypes()) {
EclipseSingularizer existingSingularizer = map.get(type);
if (existingSingularizer != null) {
EclipseSingularizer toKeep = existingSingularizer.getClass().getName().compareTo(handler.getClass().getName()) > 0 ? handler : existingSingularizer;
System.err.println("Multiple singularizers found for type " + type + "; the alphabetically first class is used: " + toKeep.getClass().getName());
map.put(type, toKeep);
} else {
map.put(type, handler);
library.addType(type);
}
}
}
}
public static EclipseSingularsRecipes get() {
return INSTANCE;
}
public String toQualified(String typeReference) {
List q = singularizableTypes.toQualifieds(typeReference);
if (q.isEmpty()) return null;
return q.get(0);
}
public EclipseSingularizer getSingularizer(String fqn) {
return singularizers.get(fqn);
}
public static final class SingularData {
private final EclipseNode annotation;
private final char[] singularName;
private final char[] pluralName;
private final char[] setterPrefix;
private final List typeArgs;
private final String targetFqn;
private final EclipseSingularizer singularizer;
private final boolean ignoreNullCollections;
private final ASTNode source;
public SingularData(EclipseNode annotation, char[] singularName, char[] pluralName, List typeArgs, String targetFqn, EclipseSingularizer singularizer, ASTNode source, boolean ignoreNullCollections) {
this(annotation, singularName, pluralName, typeArgs, targetFqn, singularizer, source, ignoreNullCollections, new char[0]);
}
public SingularData(EclipseNode annotation, char[] singularName, char[] pluralName, List typeArgs, String targetFqn, EclipseSingularizer singularizer, ASTNode source, boolean ignoreNullCollections, char[] setterPrefix) {
this.annotation = annotation;
this.singularName = singularName;
this.pluralName = pluralName;
this.typeArgs = typeArgs;
this.targetFqn = targetFqn;
this.singularizer = singularizer;
this.source = source;
this.ignoreNullCollections = ignoreNullCollections;
this.setterPrefix = setterPrefix;
}
public void setGeneratedByRecursive(ASTNode target) {
SetGeneratedByVisitor visitor = new SetGeneratedByVisitor(source);
if (target instanceof AbstractMethodDeclaration) {
((AbstractMethodDeclaration) target).traverse(visitor, (ClassScope) null);
} else if (target instanceof FieldDeclaration) {
((FieldDeclaration) target).traverse(visitor, (MethodScope) null);
} else {
target.traverse(visitor, null);
}
}
public ASTNode getSource() {
return source;
}
public EclipseNode getAnnotation() {
return annotation;
}
public char[] getSingularName() {
return singularName;
}
public char[] getPluralName() {
return pluralName;
}
public char[] getSetterPrefix() {
return setterPrefix;
}
public List getTypeArgs() {
return typeArgs;
}
public String getTargetFqn() {
return targetFqn;
}
public EclipseSingularizer getSingularizer() {
return singularizer;
}
public boolean isIgnoreNullCollections() {
return ignoreNullCollections;
}
public String getTargetSimpleType() {
int idx = targetFqn.lastIndexOf(".");
return idx == -1 ? targetFqn : targetFqn.substring(idx + 1);
}
}
public static abstract class EclipseSingularizer {
protected static final long[] NULL_POSS = {0L};
public abstract LombokImmutableList getSupportedTypes();
/** Checks if any of the to-be-generated nodes (fields, methods) already exist. If so, errors on these (singulars don't support manually writing some of it, and returns true). */
public boolean checkForAlreadyExistingNodesAndGenerateError(EclipseNode builderType, SingularData data) {
for (EclipseNode child : builderType.down()) {
switch (child.getKind()) {
case FIELD: {
FieldDeclaration fd = (FieldDeclaration) child.get();
char[] name = fd.name;
if (name == null) continue;
if (getGeneratedBy(fd) != null) continue;
for (char[] fieldToBeGenerated : listFieldsToBeGenerated(data, builderType)) {
if (!Arrays.equals(name, fieldToBeGenerated)) continue;
child.addError("Manually adding a field that @Singular @Builder would generate is not supported. If you want to manually manage the builder aspect for this field/parameter, don't use @Singular.");
return true;
}
break;
}
case METHOD: {
AbstractMethodDeclaration method = (AbstractMethodDeclaration) child.get();
char[] name = method.selector;
if (name == null) continue;
if (getGeneratedBy(method) != null) continue;
for (char[] methodToBeGenerated : listMethodsToBeGenerated(data, builderType)) {
if (!Arrays.equals(name, methodToBeGenerated)) continue;
child.addError("Manually adding a method that @Singular @Builder would generate is not supported. If you want to manually manage the builder aspect for this field/parameter, don't use @Singular.");
return true;
}
break;
}}
}
return false;
}
public List listFieldsToBeGenerated(SingularData data, EclipseNode builderType) {
return Collections.singletonList(data.pluralName);
}
public List listMethodsToBeGenerated(SingularData data, EclipseNode builderType) {
char[] p = data.pluralName;
char[] s = data.singularName;
if (Arrays.equals(p, s)) return Collections.singletonList(p);
return Arrays.asList(p, s);
}
public abstract List generateFields(SingularData data, EclipseNode builderType);
/**
* Generates the singular, plural, and clear methods for the given {@link SingularData}.
* Uses the given {@code builderType} as return type if {@code chain == true}, {@code void} otherwise.
* If you need more control over the return type and value, use
* {@link #generateMethods(SingularData, boolean, EclipseNode, boolean, TypeReferenceMaker, StatementMaker)}.
*/
public void generateMethods(final BuilderJob job, SingularData data, boolean deprecate) {
TypeReferenceMaker returnTypeMaker = new TypeReferenceMaker() {
@Override public TypeReference make() {
return job.oldChain ? cloneSelfType(job.builderType) : TypeReference.baseTypeReference(TypeIds.T_void, 0);
}
};
StatementMaker returnStatementMaker = new StatementMaker() {
@Override public ReturnStatement make() {
return job.oldChain ? new ReturnStatement(new ThisReference(0, 0), 0, 0) : null;
}
};
generateMethods(job.checkerFramework, data, deprecate, job.builderType, job.oldFluent, returnTypeMaker, returnStatementMaker, job.accessInners);
}
/**
* Generates the singular, plural, and clear methods for the given {@link SingularData}.
* Uses the given {@code returnTypeMaker} and {@code returnStatementMaker} for the generated methods.
*/
public abstract void generateMethods(CheckerFrameworkVersion cfv, SingularData data, boolean deprecate, EclipseNode builderType, boolean fluent, TypeReferenceMaker returnTypeMaker, StatementMaker returnStatementMaker, AccessLevel access);
public abstract void appendBuildCode(SingularData data, EclipseNode builderType, List statements, char[] targetVariableName, String builderVariable);
public boolean shadowedDuringBuild() {
return true;
}
public boolean requiresCleaning() {
try {
return !getClass().getMethod("appendCleaningCode", SingularData.class, EclipseNode.class, List.class).getDeclaringClass().equals(EclipseSingularizer.class);
} catch (NoSuchMethodException e) {
return false;
}
}
public void appendCleaningCode(SingularData data, EclipseNode builderType, List statements) {
}
// -- Utility methods --
protected Annotation[] generateSelfReturnAnnotations(boolean deprecate, CheckerFrameworkVersion cfv, ASTNode source) {
Annotation deprecated = deprecate ? generateDeprecatedAnnotation(source) : null;
Annotation returnsReceiver = cfv.generateReturnsReceiver() ? generateNamedAnnotation(source, CheckerFrameworkVersion.NAME__RETURNS_RECEIVER) : null;
if (deprecated == null && returnsReceiver == null) return null;
if (deprecated == null) return new Annotation[] {returnsReceiver};
if (returnsReceiver == null) return new Annotation[] {deprecated};
return new Annotation[] {deprecated, returnsReceiver};
}
/**
* Adds the requested number of type arguments to the provided type, copying each argument in {@code typeArgs}. If typeArgs is too long, the extra elements are ignored.
* If {@code typeArgs} is null or too short, {@code java.lang.Object} will be substituted for each missing type argument.
*
* @param count The number of type arguments requested.
* @param addExtends If {@code true}, all bounds are either '? extends X' or just '?'. If false, the reverse is applied, and '? extends Foo' is converted to Foo, '?' to Object, etc.
* @param node Some node in the same AST. Just used to obtain makers and contexts and such.
* @param type The type to add generics to.
* @param typeArgs the list of type args to clone.
* @param source The source annotation that is the root cause of this code generation.
*/
protected TypeReference addTypeArgs(int count, boolean addExtends, EclipseNode node, TypeReference type, List typeArgs) {
TypeReference[] clonedAndFixedArgs = createTypeArgs(count, addExtends, node, typeArgs);
if (type instanceof SingleTypeReference) {
type = new ParameterizedSingleTypeReference(((SingleTypeReference) type).token, clonedAndFixedArgs, 0, 0L);
} else if (type instanceof QualifiedTypeReference) {
QualifiedTypeReference qtr = (QualifiedTypeReference) type;
TypeReference[][] trs = new TypeReference[qtr.tokens.length][];
trs[qtr.tokens.length - 1] = clonedAndFixedArgs;
type = new ParameterizedQualifiedTypeReference(((QualifiedTypeReference) type).tokens, trs, 0, NULL_POSS);
} else {
node.addError("Don't know how to clone-and-parameterize type: " + type);
}
return type;
}
protected TypeReference[] createTypeArgs(int count, boolean addExtends, EclipseNode node, List typeArgs) {
if (count < 0) throw new IllegalArgumentException("count is negative");
if (count == 0) return null;
List arguments = new ArrayList();
if (typeArgs != null) for (TypeReference orig : typeArgs) {
Wildcard wildcard = orig instanceof Wildcard ? (Wildcard) orig : null;
if (!addExtends) {
if (wildcard != null && (wildcard.kind == Wildcard.UNBOUND || wildcard.kind == Wildcard.SUPER)) {
arguments.add(new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, NULL_POSS));
} else if (wildcard != null && wildcard.kind == Wildcard.EXTENDS) {
try {
arguments.add(copyType(wildcard.bound));
} catch (Exception e) {
arguments.add(new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, NULL_POSS));
}
} else {
arguments.add(copyType(orig));
}
} else {
if (wildcard != null && (wildcard.kind == Wildcard.UNBOUND || wildcard.kind == Wildcard.SUPER)) {
Wildcard w = new Wildcard(Wildcard.UNBOUND);
arguments.add(w);
} else if (wildcard != null && wildcard.kind == Wildcard.EXTENDS) {
arguments.add(copyType(orig));
} else {
Wildcard w = new Wildcard(Wildcard.EXTENDS);
w.bound = copyType(orig);
arguments.add(w);
}
}
if (--count == 0) break;
}
while (count-- > 0) {
if (addExtends) {
arguments.add(new Wildcard(Wildcard.UNBOUND));
} else {
arguments.add(new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, NULL_POSS));
}
}
if (arguments.isEmpty()) return null;
return arguments.toArray(new TypeReference[0]);
}
private static final char[] SIZE_TEXT = new char[] {'s', 'i', 'z', 'e'};
/** Generates 'this.name.size()' as an expression; if nullGuard is true, it's this.name == null ? 0 : this.name.size(). */
protected Expression getSize(EclipseNode builderType, char[] name, boolean nullGuard, String builderVariable) {
MessageSend invoke = new MessageSend();
Reference thisRef = getBuilderReference(builderVariable);
FieldReference thisDotName = new FieldReference(name, 0L);
thisDotName.receiver = thisRef;
invoke.receiver = thisDotName;
invoke.selector = SIZE_TEXT;
if (!nullGuard) return invoke;
Reference cdnThisRef = getBuilderReference(builderVariable);
FieldReference cdnThisDotName = new FieldReference(name, 0L);
cdnThisDotName.receiver = cdnThisRef;
NullLiteral nullLiteral = new NullLiteral(0, 0);
EqualExpression isNull = new EqualExpression(cdnThisDotName, nullLiteral, OperatorIds.EQUAL_EQUAL);
IntLiteral zeroLiteral = makeIntLiteral(new char[] {'0'}, null);
ConditionalExpression conditional = new ConditionalExpression(isNull, zeroLiteral, invoke);
return conditional;
}
protected TypeReference cloneParamType(int index, List typeArgs, EclipseNode builderType) {
if (typeArgs != null && typeArgs.size() > index) {
TypeReference originalType = typeArgs.get(index);
if (originalType instanceof Wildcard) {
Wildcard wOriginalType = (Wildcard) originalType;
if (wOriginalType.kind == Wildcard.EXTENDS) {
try {
return copyType(wOriginalType.bound);
} catch (Exception e) {
// fallthrough
}
}
} else {
return copyType(originalType);
}
}
return new QualifiedTypeReference(TypeConstants.JAVA_LANG_OBJECT, NULL_POSS);
}
/** @return a {@code SingleNameReference} to the builder in the variable builderVariable
. If {@ code builderVariable == "this"}, a {@code ThisReference} is returned. */
protected static Reference getBuilderReference(String builderVariable) {
if ("this".equals(builderVariable)) {
return new ThisReference(0, 0);
} else {
return new SingleNameReference(builderVariable.toCharArray(), 0);
}
}
protected void nullBehaviorize(EclipseNode typeNode, SingularData data, List statements, Argument arg) {
boolean ignoreNullCollections = data.isIgnoreNullCollections();
if (ignoreNullCollections) {
Expression isNotNull = new EqualExpression(new SingleNameReference(data.getPluralName(), 0L), new NullLiteral(0, 0), OperatorIds.NOT_EQUAL);
Block b = new Block(0);
b.statements = statements.toArray(new Statement[statements.size()]);
statements.clear();
statements.add(new IfStatement(isNotNull, b, 0, 0));
EclipseHandlerUtil.createRelevantNullableAnnotation(typeNode, arg);
return;
}
EclipseHandlerUtil.createRelevantNonNullAnnotation(typeNode, arg);
Statement nullCheck = EclipseHandlerUtil.generateNullCheck(null, data.getPluralName(), typeNode, "%s cannot be null");
statements.add(0, nullCheck);
}
protected abstract int getTypeArgumentsCount();
protected abstract char[][] getEmptyMakerReceiver(String targetFqn);
protected abstract char[] getEmptyMakerSelector(String targetFqn);
public MessageSend getEmptyExpression(String targetFqn, SingularData data, EclipseNode typeNode, ASTNode source) {
MessageSend send = new MessageSend();
send.receiver = generateQualifiedNameRef(source, getEmptyMakerReceiver(targetFqn));
send.selector = getEmptyMakerSelector(targetFqn);
send.typeArguments = createTypeArgs(getTypeArgumentsCount(), false, typeNode, data.getTypeArgs());
return send;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy