diff --git a/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java b/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java index dd4554e..9a3be77 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/JavaInnerBuilderHandler.java @@ -1,5 +1,6 @@ package com.github.junkfactory.innerbuilder; +import com.github.junkfactory.innerbuilder.generators.FieldCollector; import com.github.junkfactory.innerbuilder.generators.GeneratorFactory; import com.github.junkfactory.innerbuilder.generators.GeneratorParams; import com.github.junkfactory.innerbuilder.generators.PsiParams; @@ -13,15 +14,14 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.psi.JavaPsiFacade; -import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiJavaFile; +import com.intellij.util.AstLoadingFilter; import org.jetbrains.annotations.NotNull; import java.util.EnumSet; import java.util.Set; -import static com.github.junkfactory.innerbuilder.generators.FieldCollector.collectFields; import static com.github.junkfactory.innerbuilder.ui.JavaInnerBuilderOptionSelector.selectFieldsAndOptions; class JavaInnerBuilderHandler implements LanguageCodeInsightActionHandler { @@ -48,19 +48,15 @@ public boolean startInWriteAction() { } private static boolean isApplicable(final PsiFile file, final Editor editor) { - var targetElements = collectFields(file, editor); - return !targetElements.isEmpty(); + return FieldCollector.builder() + .file(file) + .editor(editor) + .build() + .hasFields(); } @Override public void invoke(@NotNull final Project project, @NotNull final Editor editor, @NotNull final PsiFile file) { - var psiDocumentManager = PsiDocumentManager.getInstance(project); - var currentDocument = psiDocumentManager.getDocument(file); - if (currentDocument == null) { - return; - } - - psiDocumentManager.commitDocument(currentDocument); if (!EditorModificationUtil.checkModificationAllowed(editor)) { return; @@ -70,29 +66,37 @@ public void invoke(@NotNull final Project project, @NotNull final Editor editor, return; } - var existingFields = collectFields(file, editor); - if (existingFields.isEmpty()) { - return; - } - - var selectedFields = selectFieldsAndOptions(existingFields, project); - if (selectedFields.isEmpty()) { - return; - } - - var psiParams = PsiParams.builder() + var fieldCollector = FieldCollector.builder() .file(file) - .selectedFields(selectedFields) - .factory(JavaPsiFacade.getElementFactory(project)) - .build(); - var generatorParams = GeneratorParams.builder() - .project(project) .editor(editor) - .psi(psiParams) - .options(currentOptions()) .build(); - var builderGenerator = generatorFactory.createInnerBuilderGenerator(generatorParams); - ApplicationManager.getApplication().runWriteAction(builderGenerator); + + AstLoadingFilter.disallowTreeLoading(() -> { + var existingFields = fieldCollector.collectFields(); + if (existingFields.isEmpty()) { + return; + } + + var selectedFields = selectFieldsAndOptions(existingFields, project); + if (selectedFields.isEmpty()) { + return; + } + + var psiParams = PsiParams.builder() + .file(file) + .selectedFields(selectedFields) + .factory(JavaPsiFacade.getElementFactory(project)) + .build(); + var generatorParams = GeneratorParams.builder() + .project(project) + .editor(editor) + .psi(psiParams) + .options(currentOptions()) + .build(); + var builderGenerator = generatorFactory.createInnerBuilderGenerator(generatorParams); + ApplicationManager.getApplication().runWriteAction(builderGenerator); + }); + } private Set currentOptions() { diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java index e33592e..741a937 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/AbstractGenerator.java @@ -15,6 +15,10 @@ abstract class AbstractGenerator implements Runnable { static final String BUILDER_METHOD_NAME = "builder"; @NonNls static final String TO_BUILDER_NAME = "toBuilder"; + @NonNls + static final String EMPTY = ""; + @NonNls + static final String SPACE = " "; protected final GeneratorFactory generatorFactory; protected final GeneratorParams generatorParams; diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderFieldsGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderFieldsGenerator.java index b105793..92c6fde 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderFieldsGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderFieldsGenerator.java @@ -8,7 +8,6 @@ import java.util.LinkedList; import java.util.List; -import java.util.Optional; class BuilderFieldsGenerator extends AbstractGenerator implements FieldsGenerator { @@ -68,9 +67,13 @@ private void deleteFieldAndMethodIfExists(PsiClass builderClass, PsiField field) if (null == field) { return; } - Optional.ofNullable(field.getCopyableUserData(UserDataKey.METHOD_REF)) - .map(m -> builderClass.findMethodBySignature(m, false)) - .ifPresent(PsiElement::delete); + var methodName = field.getCopyableUserData(UserDataKey.METHOD_REF); + if (null != methodName) { + var builderClassMethods = builderClass.findMethodsByName(methodName, false); + for (var method : builderClassMethods) { + method.delete(); + } + } field.delete(); } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java index 72dc378..17c6c6a 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/BuilderMethodsGenerator.java @@ -7,13 +7,11 @@ import com.intellij.psi.PsiField; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; -import com.intellij.psi.PsiStatement; import com.intellij.psi.util.PsiUtil; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; class BuilderMethodsGenerator extends AbstractGenerator implements MethodsGenerator { @@ -35,7 +33,7 @@ public void run() { PsiElement lastAddedElement = null; for (var field : fieldsGenerator.getFields()) { var setterMethod = generateFieldMethod(field); - field.putCopyableUserData(UserDataKey.METHOD_REF, setterMethod); + field.putCopyableUserData(UserDataKey.METHOD_REF, setterMethod.getName()); lastAddedElement = addMethod(builderClass, lastAddedElement, setterMethod, false); } @@ -86,20 +84,15 @@ private PsiMethod generateAddToCollection(PsiField field, PsiMethod fieldAddMeth .substitute(param.getType()); //now build the add method - var fieldName = "addTo" + StringUtil.capitalize(field.getName()); + var methodName = "addTo" + StringUtil.capitalize(field.getName()); + var methodText = """ + public %s %s(%s %s) { + this.%s.add(%s); + return this; + }""".formatted(BUILDER_CLASS_NAME, methodName, paramType.getPresentableText(), + param.getName().toLowerCase(), field.getName(), param.getName()); var psiElementFactory = generatorParams.psi().factory(); - var addMethod = psiElementFactory.createMethod(fieldName, builderClassParams.builderType()); - PsiUtil.setModifierProperty(addMethod, PsiModifier.PUBLIC, true); - - var addParameter = psiElementFactory.createParameter(param.getName().toLowerCase(), paramType); - addMethod.getParameterList().add(addParameter); - - var addMethodBody = Objects.requireNonNull(addMethod.getBody()); - var addBody = psiElementFactory.createStatementFromText(String.format( - "this.%s.add(%s);", field.getName(), param.getName()), addMethod); - addMethodBody.add(addBody); - addMethodBody.add(Utils.createReturnThis(psiElementFactory, addMethod)); - return addMethod; + return psiElementFactory.createMethodFromText(methodText, field); } private PsiMethod generateBuilderSetter(PsiField field) { @@ -107,53 +100,45 @@ private PsiMethod generateBuilderSetter(PsiField field) { var fieldName = Utils.hasOneLetterPrefix(field.getName()) ? Character.toLowerCase(field.getName().charAt(1)) + field.getName().substring(2) : field.getName(); + var methodText = """ + public %s %s(%s %s) { + this.%s = %s; + return this; + }""".formatted(BUILDER_CLASS_NAME, fieldName, fieldType.getPresentableText(), + fieldName, field.getName(), fieldName); var psiElementFactory = generatorParams.psi().factory(); - var setterMethod = psiElementFactory.createMethod(fieldName, builderClassParams.builderType()); - setterMethod.getModifierList().setModifierProperty(PsiModifier.PUBLIC, true); - - var setterParameter = psiElementFactory.createParameter(fieldName, fieldType); - setterMethod.getParameterList().add(setterParameter); - - var setterMethodBody = Objects.requireNonNull(setterMethod.getBody()); - var actualFieldName = "this." + fieldName; - var assignStatement = psiElementFactory.createStatementFromText(String.format( - "%s = %s;", actualFieldName, fieldName), setterMethod); - setterMethodBody.add(assignStatement); - setterMethodBody.add(Utils.createReturnThis(psiElementFactory, setterMethod)); - return setterMethod; + return psiElementFactory.createMethodFromText(methodText, field); } private PsiMethod generateBuildMethod() { var targetClass = builderClassParams.targetClass(); - var psiElementFactory = generatorParams.psi().factory(); - var targetClassType = psiElementFactory.createType(targetClass); - var buildMethod = psiElementFactory.createMethod("build", targetClassType); - var targetModifierList = Objects.requireNonNull(targetClass.getModifierList()); - Stream.of(PsiModifier.PUBLIC, PsiModifier.PACKAGE_LOCAL) - .filter(targetModifierList::hasModifierProperty) - .findFirst() - .ifPresent(modifier -> PsiUtil.setModifierProperty(buildMethod, modifier, true)); + boolean isPublic = targetModifierList.hasModifierProperty(PsiModifier.PUBLIC); - var buildMethodBody = Objects.requireNonNull(buildMethod.getBody()); + var buildMethod = new StringBuilder() + .append(isPublic ? PsiModifier.PUBLIC : EMPTY) + .append(isPublic ? SPACE : EMPTY) + .append(targetClass.getName()) + .append(" build() {"); if (generatorParams.options().contains(JavaInnerBuilderOption.WITH_VALIDATE_METHOD)) { - var validateCall = psiElementFactory.createStatementFromText("validate();", buildMethod); - buildMethodBody.add(validateCall); + buildMethod.append("validate();"); } - - final PsiStatement returnStatement; if (targetClass.isRecord()) { - var recordParameters = generatorParams.psi().selectedFields().stream() - .map(m -> m.getElement().getName()) + var recordParameters = fieldsGenerator.getFields().stream() + .map(PsiField::getName) .collect(Collectors.joining(", ")); - returnStatement = psiElementFactory.createStatementFromText(String.format( - "return new %s(%s);", targetClass.getName(), recordParameters), buildMethod); + buildMethod.append("return new ") + .append(targetClass.getName()) + .append("(") + .append(recordParameters) + .append(");"); } else { - returnStatement = psiElementFactory.createStatementFromText(String.format( - "return new %s(this);", targetClass.getName()), buildMethod); + buildMethod.append("return new ") + .append(targetClass.getName()) + .append("(this);"); } + buildMethod.append("}"); - buildMethodBody.add(returnStatement); - return buildMethod; + return generatorParams.psi().factory().createMethodFromText(buildMethod.toString(), targetClass); } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/FieldCollector.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/FieldCollector.java index 58243c4..e4aed98 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/FieldCollector.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/FieldCollector.java @@ -2,6 +2,7 @@ import com.intellij.codeInsight.generation.PsiFieldMember; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.Project; import com.intellij.openapi.util.text.StringUtil; import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; @@ -17,6 +18,7 @@ import java.util.Arrays; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import static com.intellij.openapi.util.text.StringUtil.hasLowerCaseChar; @@ -24,11 +26,25 @@ public class FieldCollector { private static final String OBJECT_CLASS_NAME = "Object"; - private FieldCollector() { + private final PsiFile file; + private final Editor editor; + + private FieldCollector(Builder builder) { + file = builder.file; + editor = builder.editor; + } + + public boolean hasFields() { + return !collectFields(true).isEmpty(); + } + + @NotNull + public List collectFields() { + return collectFields(false); } @NotNull - public static List collectFields(final PsiFile file, final Editor editor) { + private List collectFields(boolean checkOnly) { var offset = editor.getCaretModel().getOffset(); var element = file.findElementAt(offset); if (element == null) { @@ -42,34 +58,37 @@ public static List collectFields(final PsiFile file, final Edito var allFields = new ArrayList(); + var project = Objects.requireNonNull(editor.getProject()); PsiClass classToExtractFieldsFrom = clazz; while (classToExtractFieldsFrom != null) { - var classFieldMembers = collectFieldsInClass(clazz, classToExtractFieldsFrom); - allFields.addAll(0, classFieldMembers); + var classFieldMembers = collectFieldsInClass(project, clazz, classToExtractFieldsFrom); + if (checkOnly) { + return classFieldMembers.findAny().stream().toList(); + } + allFields.addAll(0, classFieldMembers.toList()); classToExtractFieldsFrom = classToExtractFieldsFrom.getSuperClass(); } return allFields; } - private static List collectFieldsInClass(PsiClass accessObjectClass, - PsiClass classToExtractFieldsFrom) { + private Stream collectFieldsInClass(Project project, PsiClass accessObjectClass, + PsiClass classToExtractFieldsFrom) { if (AbstractGenerator.BUILDER_CLASS_NAME.equals(classToExtractFieldsFrom.getName()) || OBJECT_CLASS_NAME.equals(classToExtractFieldsFrom.getName())) { - return List.of(); + return Stream.empty(); } - var helper = JavaPsiFacade.getInstance(classToExtractFieldsFrom.getProject()).getResolveHelper(); + var helper = JavaPsiFacade.getInstance(project).getResolveHelper(); return Arrays.stream(classToExtractFieldsFrom.getFields()) .filter(field -> helper.isAccessible(field, classToExtractFieldsFrom, accessObjectClass) || hasSetter(classToExtractFieldsFrom, field.getName())) .filter(field -> !field.hasModifierProperty(PsiModifier.STATIC)) .filter(field -> hasLowerCaseChar(field.getName())) .filter(field -> Objects.nonNull(field.getContainingClass())) - .map(field -> buildFieldMember(field, field.getContainingClass(), classToExtractFieldsFrom)) - .toList(); + .map(field -> buildFieldMember(field, field.getContainingClass(), classToExtractFieldsFrom)); } - private static boolean hasSetter(PsiClass clazz, String name) { + private boolean hasSetter(PsiClass clazz, String name) { for (int i = 0; i < clazz.getAllMethods().length; i++) { if (clazz.getAllMethods()[i].getName().equals(String.format("set%s", StringUtil.capitalize(name)))) { return true; @@ -79,9 +98,35 @@ private static boolean hasSetter(PsiClass clazz, String name) { return false; } - private static PsiFieldMember buildFieldMember(final PsiField field, final PsiClass containingClass, - final PsiClass clazz) { + private PsiFieldMember buildFieldMember(final PsiField field, final PsiClass containingClass, + final PsiClass clazz) { return new PsiFieldMember(field, TypeConversionUtil.getSuperClassSubstitutor(containingClass, clazz, PsiSubstitutor.EMPTY)); } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private PsiFile file; + private Editor editor; + + private Builder() { + } + + public Builder file(PsiFile file) { + this.file = file; + return this; + } + + public Builder editor(Editor editor) { + this.editor = editor; + return this; + } + + public FieldCollector build() { + return new FieldCollector(this); + } + } } diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java index 9a15935..2ec88b9 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/InnerBuilderGenerator.java @@ -31,20 +31,21 @@ public void run() { } var psiElementFactory = generatorParams.psi().factory(); var builderClass = findOrCreateBuilderClass(targetClass); - var builderType = psiElementFactory.createTypeFromText(BUILDER_CLASS_NAME, null); + var builderType = psiElementFactory.createTypeFromText(BUILDER_CLASS_NAME, targetClass); if (!targetClass.isRecord()) { - var constructor = generateConstructor(targetClass, builderType); + var constructor = generateTargetConstructor(targetClass, builderType); addMethod(targetClass, null, constructor, true); } - var newBuilderMethod = generateStaticBuilderMethod(builderType); + var newBuilderMethod = generateStaticBuilderMethod(targetClass, builderType); addMethod(targetClass, null, newBuilderMethod, false); // toBuilder method var options = generatorParams.options(); if (options.contains(JavaInnerBuilderOption.WITH_TO_BUILDER_METHOD)) { - var toBuilderMethod = generateToBuilderMethod(builderType, generatorParams.psi().selectedFields()); + var toBuilderMethod = generateToBuilderMethod(targetClass, builderType, + generatorParams.psi().selectedFields()); addMethod(targetClass, null, toBuilderMethod, true); } @@ -60,63 +61,70 @@ public void run() { CodeStyleManager.getInstance(project).reformat(builderClass); } - private PsiMethod generateToBuilderMethod(final PsiType builderType, - final Collection fields) { - var psiElementFactory = generatorParams.psi().factory(); - var toBuilderMethod = psiElementFactory.createMethod(TO_BUILDER_NAME, builderType); - PsiUtil.setModifierProperty(toBuilderMethod, PsiModifier.PUBLIC, true); - var toBuilderBody = Objects.requireNonNull(toBuilderMethod.getBody()); - var newBuilderStatement = psiElementFactory.createStatementFromText(String.format( - "%s builder = new %s();", builderType.getPresentableText(), - builderType.getPresentableText()), toBuilderMethod); - toBuilderBody.add(newBuilderStatement); - addCopyBody(fields, toBuilderMethod); - toBuilderBody.add(psiElementFactory.createStatementFromText("return builder;", toBuilderMethod)); - return toBuilderMethod; - } - - private void addCopyBody(final Collection fields, final PsiMethod method) { - var methodBody = Objects.requireNonNull(method.getBody()); - for (final PsiFieldMember member : fields) { + private PsiMethod generateToBuilderMethod(PsiClass targetClass, + PsiType builderType, + Collection fields) { + var targetModifierList = Objects.requireNonNull(targetClass.getModifierList()); + boolean isPublic = targetModifierList.hasModifierProperty(PsiModifier.PUBLIC); + var toBuilderMethod = new StringBuilder() + .append(isPublic ? PsiModifier.PUBLIC : EMPTY) + .append(isPublic ? SPACE : EMPTY) + .append(builderType.getPresentableText()) + .append(SPACE) + .append(TO_BUILDER_NAME) + .append("() {") + .append("var builder = new ") + .append(builderType.getPresentableText()) + .append("();"); + for (var member : fields) { var field = member.getElement(); - var assignStatement = generatorParams.psi().factory().createStatementFromText(String.format( - "%s%2$s = this.%3$s;", "builder.", field.getName(), field.getName()), method); - methodBody.add(assignStatement); + toBuilderMethod + .append("builder.") + .append(field.getName()) + .append(" = ") + .append(field.getName()) + .append(';') + .append(System.lineSeparator()); } + toBuilderMethod.append(" return builder; }"); + var psiElementFactory = generatorParams.psi().factory(); + return psiElementFactory.createMethodFromText(toBuilderMethod.toString(), targetClass); } - private PsiMethod generateStaticBuilderMethod(final PsiType builderType) { + private PsiMethod generateStaticBuilderMethod(PsiClass targetClass, PsiType builderType) { var psiElementFactory = generatorParams.psi().factory(); var newBuilderMethod = psiElementFactory.createMethod(BUILDER_METHOD_NAME, builderType); PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.STATIC, true); PsiUtil.setModifierProperty(newBuilderMethod, PsiModifier.PUBLIC, true); - var newBuilderMethodBody = Objects.requireNonNull(newBuilderMethod.getBody()); - var newStatement = psiElementFactory.createStatementFromText(String.format( - "return new %s();", builderType.getPresentableText()), newBuilderMethod); - newBuilderMethodBody.add(newStatement); - return newBuilderMethod; + var existingMethod = targetClass.findMethodBySignature(newBuilderMethod, true); + if (existingMethod == null) { + existingMethod = newBuilderMethod; + var newBuilderMethodBody = Objects.requireNonNull(existingMethod.getBody()); + var newStatement = psiElementFactory.createStatementFromText(String.format( + "return new %s();", builderType.getPresentableText()), newBuilderMethod); + newBuilderMethodBody.add(newStatement); + } + return existingMethod; } - private PsiMethod generateConstructor(final PsiClass targetClass, final PsiType builderType) { - var psiElementFactory = generatorParams.psi().factory(); - var constructor = psiElementFactory.createConstructor(Objects.requireNonNull(targetClass.getName())); - constructor.getModifierList().setModifierProperty(PsiModifier.PRIVATE, true); - - var builderParameter = psiElementFactory.createParameter(BUILDER_METHOD_NAME, builderType); - constructor.getParameterList().add(builderParameter); + private PsiMethod generateTargetConstructor(final PsiClass targetClass, final PsiType builderType) { + var constructor = new StringBuilder() + .append("private ") + .append(targetClass.getName()) + .append("(") + .append(builderType.getPresentableText()) + .append(" builder) {"); - var constructorBody = Objects.requireNonNull(constructor.getBody()); for (var member : generatorParams.psi().selectedFields()) { var field = member.getElement(); var setterPrototype = PropertyUtilBase.generateSetterPrototype(field); var setter = targetClass.findMethodBySignature(setterPrototype, true); var assignText = buildAssignText(field, setter); - var assignStatement = psiElementFactory.createStatementFromText(assignText, null); - constructorBody.add(assignStatement); + constructor.append(assignText).append(System.lineSeparator()); } - - return constructor; + constructor.append("}"); + return generatorParams.psi().factory().createMethodFromText(constructor.toString(), targetClass); } private static @NotNull String buildAssignText(PsiField field, PsiMethod setter) { diff --git a/src/main/java/com/github/junkfactory/innerbuilder/generators/UserDataKey.java b/src/main/java/com/github/junkfactory/innerbuilder/generators/UserDataKey.java index 294d8be..537b0a8 100644 --- a/src/main/java/com/github/junkfactory/innerbuilder/generators/UserDataKey.java +++ b/src/main/java/com/github/junkfactory/innerbuilder/generators/UserDataKey.java @@ -1,13 +1,12 @@ package com.github.junkfactory.innerbuilder.generators; import com.intellij.openapi.util.Key; -import com.intellij.psi.PsiMethod; final class UserDataKey { private UserDataKey() { } - static final Key METHOD_REF = Key.create("METHOD_REF"); + static final Key METHOD_REF = Key.create("METHOD_REF"); }