package de.plushnikov.intellij.plugin.processor.clazz.constructor;
import com.intellij.codeInsight.daemon.impl.quickfix.SafeDeleteFix;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.JavaPsiFacade;
import com.intellij.psi.PsiAnnotation;
import com.intellij.psi.PsiAnonymousClass;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiCodeBlock;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementFactory;
import com.intellij.psi.PsiField;
import com.intellij.psi.PsiMethod;
import com.intellij.psi.PsiModifier;
import com.intellij.psi.PsiModifierList;
import com.intellij.psi.PsiParameter;
import com.intellij.psi.PsiParameterList;
import com.intellij.psi.PsiType;
import com.intellij.psi.PsiTypeParameter;
import com.intellij.psi.impl.light.LightTypeParameterBuilder;
import com.intellij.psi.util.PsiTypesUtil;
import com.intellij.util.StringBuilderSpinAllocator;
import de.plushnikov.intellij.plugin.lombokconfig.ConfigKey;
import de.plushnikov.intellij.plugin.problem.ProblemBuilder;
import de.plushnikov.intellij.plugin.processor.clazz.AbstractClassProcessor;
import de.plushnikov.intellij.plugin.processor.field.AccessorsInfo;
import de.plushnikov.intellij.plugin.psi.LombokLightMethodBuilder;
import de.plushnikov.intellij.plugin.settings.ProjectSettings;
import de.plushnikov.intellij.plugin.thirdparty.LombokUtils;
import de.plushnikov.intellij.plugin.util.LombokProcessorUtil;
import de.plushnikov.intellij.plugin.util.PsiAnnotationSearchUtil;
import de.plushnikov.intellij.plugin.util.PsiAnnotationUtil;
import de.plushnikov.intellij.plugin.util.PsiClassUtil;
import de.plushnikov.intellij.plugin.util.PsiElementUtil;
import de.plushnikov.intellij.plugin.util.PsiMethodUtil;
import lombok.Value;
import lombok.experimental.NonFinal;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Base lombok processor class for constructor processing
*
* @author Plushnikov Michail
*/
public abstract class AbstractConstructorClassProcessor extends AbstractClassProcessor {
AbstractConstructorClassProcessor(@NotNull Class<? extends Annotation> supportedAnnotationClass, @NotNull Class<? extends PsiElement> supportedClass) {
super(supportedClass, supportedAnnotationClass);
}
@Override
public boolean isEnabled(@NotNull PropertiesComponent propertiesComponent) {
return ProjectSettings.isEnabled(propertiesComponent, ProjectSettings.IS_CONSTRUCTOR_ENABLED);
}
@Override
protected boolean validate(@NotNull PsiAnnotation psiAnnotation, @NotNull PsiClass psiClass, @NotNull ProblemBuilder builder) {
boolean result = true;
if (!validateAnnotationOnRightType(psiClass, builder)) {
result = false;
}
if (!validateVisibility(psiAnnotation)) {
result = false;
}
if (!validateBaseClassConstructor(psiClass, builder)) {
result = false;
}
return result;
}
private boolean validateVisibility(@NotNull PsiAnnotation psiAnnotation) {
final String visibility = LombokProcessorUtil.getAccessVisibility(psiAnnotation);
return null != visibility;
}
private boolean validateAnnotationOnRightType(@NotNull PsiClass psiClass, @NotNull ProblemBuilder builder) {
boolean result = true;
if (psiClass.isAnnotationType() || psiClass.isInterface()) {
builder.addError("Annotation is only supported on a class or enum type");
result = false;
}
return result;
}
private boolean validateBaseClassConstructor(@NotNull PsiClass psiClass, @NotNull ProblemBuilder builder) {
if (psiClass instanceof PsiAnonymousClass || psiClass.isEnum()) {
return true;
}
PsiClass baseClass = psiClass.getSuperClass();
if (baseClass == null) {
return true;
}
PsiMethod[] constructors = baseClass.getConstructors();
if (constructors.length == 0) {
return true;
}
for (PsiMethod constructor : constructors) {
final int parametersCount = constructor.getParameterList().getParametersCount();
if (parametersCount == 0 || parametersCount == 1 && constructor.isVarArgs()) {
return true;
}
}
builder.addError("Lombok needs a default constructor in the base class");
return false;
}
public boolean validateIsConstructorDefined(@NotNull PsiClass psiClass, @Nullable String staticConstructorName, @NotNull Collection<PsiField> params, @NotNull ProblemBuilder builder) {
boolean result = true;
final List<PsiType> paramTypes = new ArrayList<PsiType>(params.size());
for (PsiField param : params) {
paramTypes.add(param.getType());
}
final Collection<PsiMethod> definedConstructors = PsiClassUtil.collectClassConstructorIntern(psiClass);
final String constructorName = getConstructorName(psiClass);
final PsiMethod existedMethod = findExistedMethod(definedConstructors, constructorName, paramTypes);
if (null != existedMethod) {
if (paramTypes.isEmpty()) {
builder.addError("Constructor without parameters is already defined", new SafeDeleteFix(existedMethod));
} else {
builder.addError(String.format("Constructor with %d parameters is already defined", paramTypes.size()), new SafeDeleteFix(existedMethod));
}
result = false;
}
if (isStaticConstructor(staticConstructorName)) {
final Collection<PsiMethod> definedMethods = PsiClassUtil.collectClassStaticMethodsIntern(psiClass);
final PsiMethod existedStaticMethod = findExistedMethod(definedMethods, staticConstructorName, paramTypes);
if (null != existedStaticMethod) {
if (paramTypes.isEmpty()) {
builder.addError(String.format("Method '%s' matched staticConstructorName is already defined", staticConstructorName), new SafeDeleteFix(existedStaticMethod));
} else {
builder.addError(String.format("Method '%s' with %d parameters matched staticConstructorName is already defined", staticConstructorName, paramTypes.size()), new SafeDeleteFix(existedStaticMethod));
}
result = false;
}
}
return result;
}
@NotNull
public String getConstructorName(@NotNull PsiClass psiClass) {
return psiClass.getName();
}
@Nullable
private PsiMethod findExistedMethod(final Collection<PsiMethod> definedMethods, final String methodName, final List<PsiType> paramTypes) {
for (PsiMethod method : definedMethods) {
if (PsiElementUtil.methodMatches(method, null, null, methodName, paramTypes)) {
return method;
}
}
return null;
}
@NotNull
@SuppressWarnings("deprecation")
protected Collection<PsiField> getAllNotInitializedAndNotStaticFields(@NotNull PsiClass psiClass) {
Collection<PsiField> allNotInitializedNotStaticFields = new ArrayList<PsiField>();
final boolean classAnnotatedWithValue = PsiAnnotationSearchUtil.isAnnotatedWith(psiClass, Value.class, lombok.experimental.Value.class);
for (PsiField psiField : psiClass.getFields()) {
// skip fields named $
boolean addField = !psiField.getName().startsWith(LombokUtils.LOMBOK_INTERN_FIELD_MARKER);
final PsiModifierList modifierList = psiField.getModifierList();
if (null != modifierList) {
// skip static fields
addField &= !modifierList.hasModifierProperty(PsiModifier.STATIC);
boolean isFinal = isFieldFinal(psiField, modifierList, classAnnotatedWithValue);
// skip initialized final fields
addField &= (!isFinal || null == psiField.getInitializer());
}
if (addField) {
allNotInitializedNotStaticFields.add(psiField);
}
}
return allNotInitializedNotStaticFields;
}
@NotNull
@SuppressWarnings("deprecation")
public Collection<PsiField> getRequiredFields(@NotNull PsiClass psiClass) {
Collection<PsiField> result = new ArrayList<PsiField>();
final boolean classAnnotatedWithValue = PsiAnnotationSearchUtil.isAnnotatedWith(psiClass, Value.class, lombok.experimental.Value.class);
for (PsiField psiField : getAllNotInitializedAndNotStaticFields(psiClass)) {
final PsiModifierList modifierList = psiField.getModifierList();
if (null != modifierList) {
final boolean isFinal = isFieldFinal(psiField, modifierList, classAnnotatedWithValue);
final boolean isNonNull = PsiAnnotationSearchUtil.isAnnotatedWith(psiField, LombokUtils.NON_NULL_PATTERN);
// accept initialized final or nonnull fields
if ((isFinal || isNonNull) && null == psiField.getInitializer()) {
result.add(psiField);
}
}
}
return result;
}
private boolean isFieldFinal(@NotNull PsiField psiField, @NotNull PsiModifierList modifierList, boolean classAnnotatedWithValue) {
boolean isFinal = modifierList.hasModifierProperty(PsiModifier.FINAL);
if (!isFinal && classAnnotatedWithValue) {
isFinal = PsiAnnotationSearchUtil.isNotAnnotatedWith(psiField, NonFinal.class);
}
return isFinal;
}
@NotNull
protected Collection<PsiMethod> createConstructorMethod(@NotNull PsiClass psiClass, @PsiModifier.ModifierConstant @NotNull String methodModifier, @NotNull PsiAnnotation psiAnnotation, boolean useJavaDefaults, @NotNull Collection<PsiField> params) {
final String staticName = getStaticConstructorName(psiAnnotation);
return createConstructorMethod(psiClass, methodModifier, psiAnnotation, useJavaDefaults, params, staticName);
}
protected String getStaticConstructorName(@NotNull PsiAnnotation psiAnnotation) {
return PsiAnnotationUtil.getStringAnnotationValue(psiAnnotation, "staticName");
}
private boolean isStaticConstructor(@Nullable String staticName) {
return !StringUtil.isEmptyOrSpaces(staticName);
}
@NotNull
protected Collection<PsiMethod> createConstructorMethod(@NotNull PsiClass psiClass, @PsiModifier.ModifierConstant @NotNull String methodModifier, @NotNull PsiAnnotation psiAnnotation, boolean useJavaDefaults, @NotNull Collection<PsiField> params, @Nullable String staticName) {
final boolean staticConstructorRequired = isStaticConstructor(staticName);
final String constructorVisibility = staticConstructorRequired || psiClass.isEnum() ? PsiModifier.PRIVATE : methodModifier;
final boolean suppressConstructorProperties = useJavaDefaults || readAnnotationOrConfigProperty(psiAnnotation, psiClass, "suppressConstructorProperties", ConfigKey.ANYCONSTRUCTOR_SUPPRESS_CONSTRUCTOR_PROPERTIES);
final PsiMethod constructor = createConstructor(psiClass, constructorVisibility, suppressConstructorProperties, useJavaDefaults, params, psiAnnotation);
if (staticConstructorRequired) {
PsiMethod staticConstructor = createStaticConstructor(psiClass, staticName, useJavaDefaults, params, psiAnnotation);
return Arrays.asList(constructor, staticConstructor);
}
return Collections.singletonList(constructor);
}
private PsiMethod createConstructor(@NotNull PsiClass psiClass, @PsiModifier.ModifierConstant @NotNull String modifier, boolean suppressConstructorProperties,
boolean useJavaDefaults, @NotNull Collection<PsiField> params, @NotNull PsiAnnotation psiAnnotation) {
LombokLightMethodBuilder constructor = new LombokLightMethodBuilder(psiClass.getManager(), getConstructorName(psiClass))
.withConstructor(true)
.withContainingClass(psiClass)
.withNavigationElement(psiAnnotation)
.withModifier(modifier);
final AccessorsInfo accessorsInfo = AccessorsInfo.build(psiClass);
final PsiModifierList modifierList = constructor.getModifierList();
if (!suppressConstructorProperties && !useJavaDefaults && !params.isEmpty()) {
StringBuilder constructorPropertiesAnnotation = new StringBuilder("java.beans.ConstructorProperties( {");
for (PsiField param : params) {
constructorPropertiesAnnotation.append('"').append(accessorsInfo.removePrefix(param.getName())).append('"').append(',');
}
constructorPropertiesAnnotation.deleteCharAt(constructorPropertiesAnnotation.length() - 1);
constructorPropertiesAnnotation.append("} ) ");
modifierList.addAnnotation(constructorPropertiesAnnotation.toString());
}
addOnXAnnotations(psiAnnotation, modifierList, "onConstructor");
if (!useJavaDefaults) {
for (PsiField param : params) {
constructor.withParameter(accessorsInfo.removePrefix(param.getName()), param.getType());
}
}
final StringBuilder blockText = new StringBuilder();
for (PsiField param : params) {
final String fieldInitializer = useJavaDefaults ? PsiTypesUtil.getDefaultValueOfType(param.getType()) : accessorsInfo.removePrefix(param.getName());
blockText.append(String.format("this.%s = %s;\n", param.getName(), fieldInitializer));
}
constructor.withBody(PsiMethodUtil.createCodeBlockFromText(blockText.toString(), psiClass));
return constructor;
}
private PsiMethod createStaticConstructor(PsiClass psiClass, String staticName, boolean useJavaDefaults, Collection<PsiField> params, PsiAnnotation psiAnnotation) {
LombokLightMethodBuilder method = new LombokLightMethodBuilder(psiClass.getManager(), staticName)
.withContainingClass(psiClass)
.withNavigationElement(psiAnnotation)
.withModifier(PsiModifier.PUBLIC, PsiModifier.STATIC);
final PsiElementFactory factory = JavaPsiFacade.getElementFactory(psiClass.getProject());
final PsiType[] methodTypeParameterTypes;
if (psiClass.hasTypeParameters()) {
final PsiTypeParameter[] psiClassTypeParameters = psiClass.getTypeParameters();
// create new type parameters
for (int index = 0; index < psiClassTypeParameters.length; index++) {
final PsiTypeParameter psiClassTypeParameter = psiClassTypeParameters[index];
method.withTypeParameter(new LightTypeParameterBuilder(psiClassTypeParameter.getName(), method, index));
}
// create psiType for each of type parameter
final PsiTypeParameter[] methodTypeParameters = method.getTypeParameters();
methodTypeParameterTypes = new PsiType[methodTypeParameters.length];
for (int index = 0; index < methodTypeParameters.length; index++) {
methodTypeParameterTypes[index] = factory.createType(methodTypeParameters[index]);
}
} else {
methodTypeParameterTypes = PsiType.EMPTY_ARRAY;
}
final PsiType returnType = factory.createType(psiClass, methodTypeParameterTypes);
method.withMethodReturnType(returnType);
if (!useJavaDefaults) {
for (PsiField param : params) {
method.withParameter(param.getName(), chooseType(param.getType(), methodTypeParameterTypes));
}
}
method.withBody(createStaticCodeBlock(returnType, useJavaDefaults, method.getParameterList()));
return method;
}
@NotNull
private PsiType chooseType(@NotNull PsiType typeOfField, @NotNull PsiType[] typeParameterTypes) {
final String presentableText = typeOfField.getPresentableText();
for (PsiType typeParameterType : typeParameterTypes) {
if (presentableText.equals(typeParameterType.getPresentableText())) {
return typeParameterType;
}
}
return typeOfField;
}
@NotNull
private PsiCodeBlock createStaticCodeBlock(@NotNull PsiType psiType, boolean useJavaDefaults, @NotNull final PsiParameterList parameterList) {
final String blockText;
if (isShouldGenerateFullBodyBlock()) {
final String psiClassName = psiType.getPresentableText();
final String paramsText = useJavaDefaults ? "" : joinParameters(parameterList);
blockText = String.format("return new %s(%s);", psiClassName, paramsText);
} else {
blockText = "return null;";
}
return PsiMethodUtil.createCodeBlockFromText(blockText, parameterList);
}
private String joinParameters(PsiParameterList parameterList) {
final StringBuilder builder = StringBuilderSpinAllocator.alloc();
try {
for (PsiParameter psiParameter : parameterList.getParameters()) {
builder.append(psiParameter.getName()).append(',');
}
if (parameterList.getParameters().length > 0) {
builder.deleteCharAt(builder.length() - 1);
}
return builder.toString();
} finally {
StringBuilderSpinAllocator.dispose(builder);
}
}
}