Skip to content

Commit

Permalink
Add Code Action: Generate Constructor
Browse files Browse the repository at this point in the history
Signed-off-by: Jinbo Wang <[email protected]>
  • Loading branch information
testforstephen committed May 14, 2019
1 parent b75859a commit 7a70139
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ public interface JavaCodeActionKind {
*/
public static final String SOURCE_GENERATE_TO_STRING = SOURCE_GENERATE + ".toString";

/**
* Generate Constructor kind
*/
public static final String SOURCE_GENERATE_CONSTRUCTOR = SOURCE_GENERATE + ".constructor";

/**
* Override/Implement methods kind
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
/*******************************************************************************
* Copyright (c) 2019 Microsoft Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Microsoft Corporation - initial API and implementation
*******************************************************************************/

package org.eclipse.jdt.ls.core.internal.handlers;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.jdt.core.IField;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.internal.corext.codemanipulation.AddCustomConstructorOperation;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2Core;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.dom.Bindings;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser;
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
import org.eclipse.jdt.ls.core.internal.handlers.JdtDomModels.LspMethodBinding;
import org.eclipse.jdt.ls.core.internal.handlers.JdtDomModels.LspVariableBinding;
import org.eclipse.jdt.ls.core.internal.preferences.Preferences;
import org.eclipse.jdt.ls.core.internal.text.correction.SourceAssistProcessor;
import org.eclipse.lsp4j.CodeActionParams;
import org.eclipse.lsp4j.WorkspaceEdit;
import org.eclipse.text.edits.TextEdit;

public class GenerateConstructorHandler {

public static CheckConstructorResponse checkConstructorStatus(CodeActionParams params) {
IType type = SourceAssistProcessor.getSelectionType(params);
return checkConstructorStatus(type);
}

public static CheckConstructorResponse checkConstructorStatus(IType type) {
if (type == null || type.getCompilationUnit() == null) {
return new CheckConstructorResponse();
}

try {
RefactoringASTParser astParser = new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL);
CompilationUnit astRoot = astParser.parse(type.getCompilationUnit(), true);
ITypeBinding typeBinding = ASTNodes.getTypeBinding(astRoot, type);
if (typeBinding == null) {
return new CheckConstructorResponse();
}

IMethodBinding[] superConstructors = getVisibleConstructors(astRoot, typeBinding);
Map<IJavaElement, IVariableBinding> fieldsToBindings = new HashMap<>();
for (IVariableBinding field : typeBinding.getDeclaredFields()) {
if (field.isSynthetic() || Modifier.isStatic(field.getModifiers())) {
continue;
}

if (Modifier.isFinal(field.getModifiers())) {
ASTNode declaringNode = astRoot.findDeclaringNode(field);
// Do not add final fields which have been set in the <clinit>
if (declaringNode instanceof VariableDeclarationFragment && ((VariableDeclarationFragment) declaringNode).getInitializer() != null) {
continue;
}
}

fieldsToBindings.put(field.getJavaElement(), field);
}

List<IVariableBinding> fields = new ArrayList<>();
// Sort the fields by the order in which they appear in the source or class file.
for (IField field : type.getFields()) {
IVariableBinding fieldBinding = fieldsToBindings.remove(field);
if (fieldBinding != null) {
fields.add(fieldBinding);
}
}

//@formatter:off
return new CheckConstructorResponse(
Arrays.stream(superConstructors).map(binding -> new LspMethodBinding(binding)).toArray(LspMethodBinding[]::new),
fields.stream().map(binding -> new LspVariableBinding(binding)).toArray(LspVariableBinding[]::new)
);
//@formatter:on
} catch (JavaModelException e) {
JavaLanguageServerPlugin.logException("Failed to check Constructor status", e);
}

return new CheckConstructorResponse();
}

private static IMethodBinding getObjectConstructor(AST ast) {
final ITypeBinding binding = ast.resolveWellKnownType("java.lang.Object");
return Bindings.findMethodInType(binding, "Object", new ITypeBinding[0]);
}

private static IMethodBinding[] getVisibleConstructors(CompilationUnit astRoot, ITypeBinding typeBinding) {
if (typeBinding.isEnum()) {
return new IMethodBinding[] { getObjectConstructor(astRoot.getAST()) };
} else {
return StubUtility2Core.getVisibleConstructors(typeBinding, false, true);
}
}

public static WorkspaceEdit generateConstructor(GenerateConstructorParams params) {
IType type = SourceAssistProcessor.getSelectionType(params.context);
TextEdit edit = generateConstructor(type, params.constructor, params.fields);
return (edit == null) ? null : SourceAssistProcessor.convertToWorkspaceEdit(type.getCompilationUnit(), edit);
}

public static TextEdit generateConstructor(IType type, LspMethodBinding constructor, LspVariableBinding[] fields) {
Preferences preferences = JavaLanguageServerPlugin.getPreferencesManager().getPreferences();
CodeGenerationSettings settings = new CodeGenerationSettings();
settings.createComments = preferences.isCodeGenerationTemplateGenerateComments();
return generateConstructor(type, constructor, fields, settings);
}

public static TextEdit generateConstructor(IType type, LspMethodBinding constructor, LspVariableBinding[] fields, CodeGenerationSettings settings) {
if (type == null || type.getCompilationUnit() == null || constructor == null) {
return null;
}

try {
RefactoringASTParser astParser = new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL);
CompilationUnit astRoot = astParser.parse(type.getCompilationUnit(), true);
ITypeBinding typeBinding = ASTNodes.getTypeBinding(astRoot, type);
if (typeBinding != null) {
Map<String, IVariableBinding> fieldBindings = new HashMap<>();
for (IVariableBinding binding : typeBinding.getDeclaredFields()) {
fieldBindings.put(binding.getKey(), binding);
}

IVariableBinding[] selectedFields = Arrays.stream(fields).map(field -> fieldBindings.get(field.bindingKey)).filter(binding -> binding != null).toArray(IVariableBinding[]::new);
IMethodBinding[] superConstructors = getVisibleConstructors(astRoot, typeBinding);
Optional<IMethodBinding> selectedSuperConstructor = Arrays.stream(superConstructors).filter(superConstructor -> compareConstructor(superConstructor, constructor)).findAny();
if (selectedSuperConstructor.isPresent()) {
IMethodBinding superConstructor = selectedSuperConstructor.get();
AddCustomConstructorOperation constructorOperation = new AddCustomConstructorOperation(astRoot, typeBinding, selectedFields, superConstructor, null, settings, false, false);
constructorOperation.setOmitSuper(superConstructor.getParameterTypes().length == 0);
constructorOperation.setVisibility(typeBinding.isEnum() ? Modifier.PRIVATE : Modifier.PUBLIC);
constructorOperation.run(null);
return constructorOperation.getResultingEdit();
}
}
} catch (CoreException e) {
JavaLanguageServerPlugin.logException("Failed to generate constructor", e);
}

return null;
}

private static boolean compareConstructor(IMethodBinding binding, LspMethodBinding lspBinding) {
if (lspBinding == null) {
return binding == null;
}

if (binding == null) {
return lspBinding == null;
}

String[] parameters = Arrays.stream(binding.getParameterTypes()).map(type -> type.getName()).toArray(String[]::new);
return Arrays.equals(parameters, lspBinding.parameters);
}

public static class CheckConstructorResponse {
public LspMethodBinding[] constructors = new LspMethodBinding[0];
public LspVariableBinding[] fields = new LspVariableBinding[0];

public CheckConstructorResponse() {
}

public CheckConstructorResponse(LspMethodBinding[] constructors, LspVariableBinding[] fields) {
this.constructors = constructors;
this.fields = fields;
}
}

public static class GenerateConstructorParams {
public CodeActionParams context;
public LspMethodBinding constructor;
public LspVariableBinding[] fields;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import org.eclipse.jdt.ls.core.internal.ServiceStatus;
import org.eclipse.jdt.ls.core.internal.codemanipulation.GenerateGetterSetterOperation.AccessorField;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateAccessorsHandler.GenerateAccessorsParams;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler.CheckConstructorResponse;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler.GenerateConstructorParams;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateToStringHandler.CheckToStringResponse;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateToStringHandler.GenerateToStringParams;
import org.eclipse.jdt.ls.core.internal.handlers.HashCodeEqualsHandler.CheckHashCodeEqualsResponse;
Expand Down Expand Up @@ -813,6 +815,18 @@ public CompletableFuture<WorkspaceEdit> generateAccessors(GenerateAccessorsParam
return computeAsync((monitor) -> GenerateAccessorsHandler.generateAccessors(params));
}

@Override
public CompletableFuture<CheckConstructorResponse> checkConstructorStatus(CodeActionParams params) {
logInfo(">> java/checkConstructorStatus");
return computeAsync((monitor) -> GenerateConstructorHandler.checkConstructorStatus(params));
}

@Override
public CompletableFuture<WorkspaceEdit> generateConstructor(GenerateConstructorParams params) {
logInfo(">> java/generateConstructor");
return computeAsync((monitor) -> GenerateConstructorHandler.generateConstructor(params));
}

public void sendStatus(ServiceStatus serverStatus, String status) {
if (client != null) {
client.sendStatus(serverStatus, status);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.Modifier;
Expand All @@ -34,6 +35,18 @@ public LspVariableBinding(IVariableBinding binding) {
}
}

public static class LspMethodBinding {
public String bindingKey;
public String name;
public String[] parameters;

public LspMethodBinding(IMethodBinding binding) {
this.bindingKey = binding.getKey();
this.name = binding.getName();
this.parameters = Stream.of(binding.getParameterTypes()).map(type -> type.getName()).toArray(String[]::new);
}
}

public static IVariableBinding[] convertToVariableBindings(ITypeBinding typeBinding, LspVariableBinding[] fields) {
Set<String> bindingKeys = Stream.of(fields).map((field) -> field.bindingKey).collect(Collectors.toSet());
return Arrays.stream(typeBinding.getDeclaredFields()).filter(f -> bindingKeys.contains(f.getKey())).toArray(IVariableBinding[]::new);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.eclipse.jdt.ls.core.internal.BuildWorkspaceStatus;
import org.eclipse.jdt.ls.core.internal.codemanipulation.GenerateGetterSetterOperation.AccessorField;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateAccessorsHandler.GenerateAccessorsParams;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler.CheckConstructorResponse;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler.GenerateConstructorParams;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateToStringHandler.CheckToStringResponse;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateToStringHandler.GenerateToStringParams;
import org.eclipse.jdt.ls.core.internal.handlers.HashCodeEqualsHandler.CheckHashCodeEqualsResponse;
Expand Down Expand Up @@ -76,4 +78,10 @@ public interface JavaProtocolExtensions {

@JsonRequest
CompletableFuture<WorkspaceEdit> generateAccessors(GenerateAccessorsParams params);

@JsonRequest
CompletableFuture<CheckConstructorResponse> checkConstructorStatus(CodeActionParams params);

@JsonRequest
CompletableFuture<WorkspaceEdit> generateConstructor(GenerateConstructorParams params);
}
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ public boolean isAdvancedGenerateAccessorsSupported() {
return Boolean.parseBoolean(extendedClientCapabilities.getOrDefault("advancedGenerateAccessorsSupport", "false").toString());
}

public boolean isGenerateConstructorPromptSupported() {
return Boolean.parseBoolean(extendedClientCapabilities.getOrDefault("generateConstructorPromptSupport", "false").toString());
}

public boolean isSupportsCompletionDocumentationMarkdown() {
//@formatter:off
return v3supported && capabilities.getTextDocument().getCompletion() != null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ private ActionMessages() {
public static String GenerateGetterSetterAction_ellipsisLabel;
public static String GenerateHashCodeEqualsAction_label;
public static String GenerateToStringAction_label;
public static String GenerateConstructorAction_label;
public static String GenerateConstructorAction_ellipsisLabel;

static {
NLS.initializeMessages(BUNDLE_NAME, ActionMessages.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ OverrideMethodsAction_label=Override/Implement Methods...
GenerateGetterSetterAction_label=Generate Getters and Setters
GenerateGetterSetterAction_ellipsisLabel=Generate Getters and Setters...
GenerateHashCodeEqualsAction_label=Generate hashCode() and equals()...
GenerateToStringAction_label=Generate toString()...
GenerateToStringAction_label=Generate toString()...
GenerateConstructorAction_label=Generate Constructor
GenerateConstructorAction_ellipsisLabel=Generate Constructor...
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import org.eclipse.jdt.ls.core.internal.corrections.IInvocationContext;
import org.eclipse.jdt.ls.core.internal.corrections.InnovationContext;
import org.eclipse.jdt.ls.core.internal.handlers.CodeActionHandler;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateConstructorHandler.CheckConstructorResponse;
import org.eclipse.jdt.ls.core.internal.handlers.GenerateToStringHandler;
import org.eclipse.jdt.ls.core.internal.handlers.JdtDomModels.LspVariableBinding;
import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager;
Expand All @@ -73,6 +75,7 @@ public class SourceAssistProcessor {
public static final String COMMAND_ID_ACTION_ORGANIZEIMPORTS = "java.action.organizeImports";
public static final String COMMAND_ID_ACTION_GENERATETOSTRINGPROMPT = "java.action.generateToStringPrompt";
public static final String COMMAND_ID_ACTION_GENERATEACCESSORSPROMPT = "java.action.generateAccessorsPrompt";
public static final String COMMAND_ID_ACTION_GENERATECONSTRUCTORPROMPT = "java.action.generateConstructorPrompt";

private PreferenceManager preferenceManager;

Expand Down Expand Up @@ -131,6 +134,10 @@ public List<Either<Command, CodeAction>> getSourceActionCommands(CodeActionParam
}
}

// Generate Constructor
Optional<Either<Command, CodeAction>> generateConstructor = getGenerateConstructorAction(params, context, type);
addSourceActionCommand($, params.getContext(), generateConstructor);

return $;
}

Expand Down Expand Up @@ -275,6 +282,37 @@ private Optional<Either<Command, CodeAction>> getGenerateToStringAction(CodeActi
}
}

private Optional<Either<Command, CodeAction>> getGenerateConstructorAction(CodeActionParams params, IInvocationContext context, IType type) {
try {
if (type == null || type.isAnnotation() || type.isInterface() || type.isAnonymous() || type.getCompilationUnit() == null) {
return Optional.empty();
}
} catch (JavaModelException e) {
return Optional.empty();
}

if (preferenceManager.getClientPreferences().isGenerateConstructorPromptSupported()) {
CheckConstructorResponse status = GenerateConstructorHandler.checkConstructorStatus(type);
if (status.constructors.length == 1 && status.fields.length == 0) {
TextEdit edit = GenerateConstructorHandler.generateConstructor(type, status.constructors[0], status.fields);
return convertToWorkspaceEditAction(params.getContext(), type.getCompilationUnit(), ActionMessages.GenerateConstructorAction_label, JavaCodeActionKind.SOURCE_GENERATE_CONSTRUCTOR, edit);
}

Command command = new Command(ActionMessages.GenerateConstructorAction_ellipsisLabel, COMMAND_ID_ACTION_GENERATECONSTRUCTORPROMPT, Collections.singletonList(params));
if (preferenceManager.getClientPreferences().isSupportedCodeActionKind(JavaCodeActionKind.SOURCE_GENERATE_CONSTRUCTOR)) {
CodeAction codeAction = new CodeAction(ActionMessages.GenerateConstructorAction_ellipsisLabel);
codeAction.setKind(JavaCodeActionKind.SOURCE_GENERATE_CONSTRUCTOR);
codeAction.setCommand(command);
codeAction.setDiagnostics(Collections.EMPTY_LIST);
return Optional.of(Either.forRight(codeAction));
} else {
return Optional.of(Either.forLeft(command));
}
}

return Optional.empty();
}

private Optional<Either<Command, CodeAction>> convertToWorkspaceEditAction(CodeActionContext context, ICompilationUnit cu, String name, String kind, TextEdit edit) {
WorkspaceEdit workspaceEdit = convertToWorkspaceEdit(cu, edit);
if (!ChangeUtil.hasChanges(workspaceEdit)) {
Expand Down
Loading

0 comments on commit 7a70139

Please sign in to comment.