blob: 1df7aeccb31edd8a0228bb1743cd82598f7c4506 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2018 IBM 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:
* IBM Corporation - initial API and implementation
* Stephan Herrmann - Contribution for Bug 463360 - [override method][null] generating method override should not create redundant null annotations
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.eclipse.swt.graphics.Image;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.text.edits.MalformedTreeException;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.window.Window;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension4;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jdt.core.CompletionProposal;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.ISourceRange;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.Signature;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
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.MethodDeclaration;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.core.dom.rewrite.ITrackedNodePosition;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
import org.eclipse.jdt.core.formatter.CodeFormatter;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.eclipse.jdt.internal.core.manipulation.util.Strings;
import org.eclipse.jdt.internal.corext.codemanipulation.CodeGenerationSettings;
import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext;
import org.eclipse.jdt.internal.corext.codemanipulation.StubUtility2;
import org.eclipse.jdt.internal.corext.dom.IASTSharedValues;
import org.eclipse.jdt.internal.corext.util.CodeFormatterUtil;
import org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext;
import org.eclipse.jdt.internal.ui.JavaPlugin;
import org.eclipse.jdt.internal.ui.JavaPluginImages;
import org.eclipse.jdt.internal.ui.dialogs.OverrideMethodDialog;
import org.eclipse.jdt.internal.ui.preferences.JavaPreferencesSettings;
import org.eclipse.jdt.internal.ui.preferences.formatter.FormatterProfileManager;
public class AnonymousTypeCompletionProposal extends JavaTypeCompletionProposal implements ICompletionProposalExtension4 {
private final String fDeclarationSignature;
private final IType fSuperType;
private boolean fIsContextInformationComputed;
private int fContextInformationPosition;
private ImportRewrite fImportRewrite;
public AnonymousTypeCompletionProposal(IJavaProject jproject, ICompilationUnit cu, JavaContentAssistInvocationContext invocationContext, int start, int length, String constructorCompletion, StyledString displayName, String declarationSignature, IType superType, int relevance) {
super(constructorCompletion, cu, start, length, null, displayName, relevance, null, invocationContext);
Assert.isNotNull(declarationSignature);
Assert.isNotNull(jproject);
Assert.isNotNull(cu);
Assert.isNotNull(superType);
fDeclarationSignature= declarationSignature;
fSuperType= superType;
setCursorPosition(constructorCompletion.indexOf('(') + 1);
}
private String createDummyType(String name) throws JavaModelException {
StringBuffer buffer= new StringBuffer();
buffer.append("abstract class "); //$NON-NLS-1$
buffer.append(name);
if (fSuperType.isInterface())
buffer.append(" implements "); //$NON-NLS-1$
else
buffer.append(" extends "); //$NON-NLS-1$
if (fDeclarationSignature != null)
buffer.append(Signature.toString(fDeclarationSignature));
else
buffer.append(fSuperType.getFullyQualifiedParameterizedName());
buffer.append(" {"); //$NON-NLS-1$
buffer.append("\n"); // Using newline is ok since source is used in dummy compilation unit //$NON-NLS-1$
buffer.append("}"); //$NON-NLS-1$
return buffer.toString();
}
private String createNewBody(ImportRewrite importRewrite, int offset) throws CoreException {
if (importRewrite == null)
return null;
ICompilationUnit workingCopy= null;
try {
String name= "Type" + System.currentTimeMillis(); //$NON-NLS-1$
workingCopy= fCompilationUnit.getPrimary().getWorkingCopy(null);
ISourceRange range= fSuperType.getSourceRange();
boolean sameUnit= range != null && fCompilationUnit.equals(fSuperType.getCompilationUnit());
// creates a type that extends the super type
String dummyClassContent= createDummyType(name);
StringBuffer workingCopyContents= new StringBuffer(fCompilationUnit.getSource());
int insertPosition;
if (sameUnit) {
insertPosition= range.getOffset() + range.getLength();
} else {
ISourceRange firstTypeRange= fCompilationUnit.getTypes()[0].getSourceRange();
insertPosition= firstTypeRange.getOffset();
}
if (fSuperType.isLocal()) {
// add an extra block: helps the AST to recover
workingCopyContents.insert(insertPosition, '{' + dummyClassContent + '}');
insertPosition++;
if(offset > insertPosition) {
offset += dummyClassContent.length()+2;
}
} else {
/*
* The two empty lines are added because the trackedDeclaration uses the covered range
* and hence would also included comments that directly follow the dummy class.
*/
workingCopyContents.insert(insertPosition, dummyClassContent + "\n\n"); //$NON-NLS-1$
if(offset > insertPosition) {
offset += dummyClassContent.length()+2;
}
}
workingCopy.getBuffer().setContents(workingCopyContents.toString());
ASTParser parser= ASTParser.newParser(IASTSharedValues.SHARED_AST_LEVEL);
parser.setResolveBindings(true);
parser.setStatementsRecovery(true);
parser.setSource(workingCopy);
CompilationUnit astRoot= (CompilationUnit) parser.createAST(new NullProgressMonitor());
ImportRewriteContext context=new ContextSensitiveImportRewriteContext(astRoot, offset, importRewrite);
ASTNode newType= NodeFinder.perform(astRoot, insertPosition, dummyClassContent.length());
if (!(newType instanceof AbstractTypeDeclaration))
return null;
AbstractTypeDeclaration declaration= (AbstractTypeDeclaration) newType;
ITypeBinding dummyTypeBinding= declaration.resolveBinding();
if (dummyTypeBinding == null)
return null;
IMethodBinding[] bindings= StubUtility2.getOverridableMethods(astRoot.getAST(), dummyTypeBinding, true);
if (fSuperType.isInterface()) {
ITypeBinding[] dummySuperInterfaces= dummyTypeBinding.getInterfaces();
if (dummySuperInterfaces.length == 0 || dummySuperInterfaces.length == 1 && dummySuperInterfaces[0].isRawType())
bindings= new IMethodBinding[0];
} else {
ITypeBinding dummySuperclass= dummyTypeBinding.getSuperclass();
if (dummySuperclass == null || dummySuperclass.isRawType())
bindings= new IMethodBinding[0];
}
CodeGenerationSettings settings= JavaPreferencesSettings.getCodeGenerationSettings(fSuperType.getJavaProject());
IMethodBinding[] methodsToOverride= null;
IType type= null;
if (!fSuperType.isInterface() && !fSuperType.isAnnotation()) {
IJavaElement typeElement= dummyTypeBinding.getJavaElement();
// add extra checks here as the recovered code is fragile
if (typeElement instanceof IType && name.equals(typeElement.getElementName()) && typeElement.exists()) {
type= (IType) typeElement;
}
}
if (type != null) {
OverrideMethodDialog dialog= new OverrideMethodDialog(JavaPlugin.getActiveWorkbenchShell(), null, type, true);
dialog.setGenerateComment(false);
dialog.setElementPositionEnabled(false);
if (dialog.open() == Window.OK) {
Object[] selection= dialog.getResult();
ArrayList<Object> result= new ArrayList<>(selection.length);
for (int i= 0; i < selection.length; i++) {
if (selection[i] instanceof IMethodBinding)
result.add(selection[i]);
}
methodsToOverride= result.toArray(new IMethodBinding[result.size()]);
settings.createComments= dialog.getGenerateComment();
} else {
// cancelled
setReplacementString(""); //$NON-NLS-1$
setReplacementLength(0);
return null;
}
} else {
settings.createComments= false;
List<IMethodBinding> result= new ArrayList<>();
for (int i= 0; i < bindings.length; i++) {
IMethodBinding curr= bindings[i];
if (Modifier.isAbstract(curr.getModifiers()))
result.add(curr);
}
methodsToOverride= result.toArray(new IMethodBinding[result.size()]);
}
ASTNode focusNode; // used to find @NonNullByDefault effective
if (fCompilationUnit.getJavaProject().getOption(JavaCore.COMPILER_ANNOTATION_NULL_ANALYSIS, true).equals(JavaCore.ENABLED)) {
focusNode= NodeFinder.perform(astRoot, getReplacementOffset()+dummyClassContent.length(), 0);
} else {
focusNode= null;
}
ASTRewrite rewrite= ASTRewrite.create(astRoot.getAST());
ITrackedNodePosition trackedDeclaration= rewrite.track(declaration);
ListRewrite rewriter= rewrite.getListRewrite(declaration, declaration.getBodyDeclarationsProperty());
for (int i= 0; i < methodsToOverride.length; i++) {
IMethodBinding curr= methodsToOverride[i];
MethodDeclaration stub= StubUtility2.createImplementationStub(workingCopy, rewrite, importRewrite, context, curr, dummyTypeBinding, settings, dummyTypeBinding.isInterface(), focusNode);
rewriter.insertFirst(stub, null);
}
IDocument document= new Document(workingCopy.getSource());
try {
rewrite.rewriteAST().apply(document);
int bodyStart= trackedDeclaration.getStartPosition() + dummyClassContent.indexOf('{');
int bodyEnd= trackedDeclaration.getStartPosition() + trackedDeclaration.getLength();
return document.get(bodyStart, bodyEnd - bodyStart);
} catch (MalformedTreeException exception) {
JavaPlugin.log(exception);
} catch (BadLocationException exception) {
JavaPlugin.log(exception);
}
return null;
} finally {
if (workingCopy != null)
workingCopy.discardWorkingCopy();
}
}
protected Image getImageForType(IType type) {
String imageName= JavaPluginImages.IMG_OBJS_CLASS; // default
try {
if (type.isAnnotation()) {
imageName= JavaPluginImages.IMG_OBJS_ANNOTATION;
} else if (type.isInterface()) {
imageName= JavaPluginImages.IMG_OBJS_INTERFACE;
}
} catch (JavaModelException e) {
JavaPlugin.log(e);
}
return JavaPluginImages.get(imageName);
}
@Override
public Image getImage() {
Image image= super.getImage();
if (image == null) {
image= getImageForType(fSuperType);
setImage(image);
}
return image;
}
/*
* @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension4#isAutoInsertable()
*/
@Override
public boolean isAutoInsertable() {
return false;
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#isOffsetValid(int)
* @since 3.5
*/
@Override
protected boolean isOffsetValid(int offset) {
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
return super.isOffsetValid(offset);
return coreProposal.getRequiredProposals()[0].getReplaceStart() <= offset;
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#getPrefixCompletionStart(org.eclipse.jface.text.IDocument, int)
* @since 3.5
*/
@Override
public int getPrefixCompletionStart(IDocument document, int completionOffset) {
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
return super.getPrefixCompletionStart(document, completionOffset);
return coreProposal.getRequiredProposals()[0].getReplaceStart();
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#getPrefixCompletionText(org.eclipse.jface.text.IDocument, int)
* @since 3.5
*/
@Override
public CharSequence getPrefixCompletionText(IDocument document, int completionOffset) {
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
return super.getPrefixCompletionText(document, completionOffset);
return String.valueOf(coreProposal.getName());
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#getPrefix(org.eclipse.jface.text.IDocument, int)
* @since 3.5
*/
@Override
protected String getPrefix(IDocument document, int offset) {
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
return super.getPrefix(document, offset);
int replacementOffset= coreProposal.getRequiredProposals()[0].getReplaceStart();
try {
int length= offset - replacementOffset;
if (length > 0)
return document.get(replacementOffset, length);
} catch (BadLocationException x) {
}
return ""; //$NON-NLS-1$
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#isValidPrefix(java.lang.String)
* @since 3.5
*/
@Override
protected boolean isValidPrefix(String prefix) {
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
if (coreProposal.getKind() != CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION)
return super.isValidPrefix(prefix);
return super.isValidPrefix(prefix) || isPrefix(prefix, String.valueOf(coreProposal.getName()));
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#apply(org.eclipse.jface.text.IDocument, char, int)
* @since 3.5
*/
@Override
public void apply(IDocument document, char trigger, int offset) {
super.apply(document, trigger, offset);
LinkedModeModel.closeAllModels(document);
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.JavaTypeCompletionProposal#updateReplacementString(org.eclipse.jface.text.IDocument, char, int, org.eclipse.jdt.core.dom.rewrite.ImportRewrite)
*/
@Override
protected boolean updateReplacementString(IDocument document, char trigger, int offset, ImportRewrite impRewrite) throws CoreException, BadLocationException {
fImportRewrite= impRewrite;
String newBody= createNewBody(impRewrite, offset);
if (newBody == null)
return false;
CompletionProposal coreProposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
boolean isAnonymousConstructorInvoc= coreProposal.getKind() == CompletionProposal.ANONYMOUS_CLASS_CONSTRUCTOR_INVOCATION;
boolean replacementStringEndsWithParentheses= isAnonymousConstructorInvoc || getReplacementString().endsWith(")"); //$NON-NLS-1$
// construct replacement text: an expression to be formatted
StringBuffer buf= new StringBuffer("new A("); //$NON-NLS-1$
if (!replacementStringEndsWithParentheses || isAnonymousConstructorInvoc)
buf.append(')');
buf.append(newBody);
// use the code formatter
String lineDelim= TextUtilities.getDefaultLineDelimiter(document);
final IJavaProject project= fCompilationUnit.getJavaProject();
IRegion lineInfo= document.getLineInformationOfOffset(getReplacementOffset());
int indent= Strings.computeIndentUnits(document.get(lineInfo.getOffset(), lineInfo.getLength()), project);
Map<String, String> options= project != null ? FormatterProfileManager.getProjectSettings(project) : JavaCore.getOptions();
options.put(DefaultCodeFormatterConstants.FORMATTER_INDENT_EMPTY_LINES, DefaultCodeFormatterConstants.TRUE);
String replacementString= CodeFormatterUtil.format(CodeFormatter.K_EXPRESSION, buf.toString(), 0, lineDelim, options);
int lineEndOffset= lineInfo.getOffset() + lineInfo.getLength();
int p= offset;
char ch= document.getChar(p);
while (p < lineEndOffset) {
if (ch == '(' || ch == ')' || ch == ';' || ch == ',')
break;
ch= document.getChar(++p);
}
if (ch != ';' && ch != ',' && ch != ')')
replacementString= replacementString + ';';
replacementString= Strings.changeIndent(replacementString, 0, project, CodeFormatterUtil.createIndentString(indent, project), lineDelim);
int beginIndex= replacementString.indexOf('(');
if (!isAnonymousConstructorInvoc)
beginIndex++;
replacementString= replacementString.substring(beginIndex);
int pos= offset;
if (isAnonymousConstructorInvoc && (insertCompletion() ^ isInsertModeToggled())) {
// Keep existing code
int endPos= pos;
ch= document.getChar(endPos);
while (endPos < lineEndOffset && ch != '(' && ch != ')' && ch != ';' && ch != ',' && !Character.isWhitespace(ch))
ch= document.getChar(++endPos);
int keepLength= endPos - pos;
if (keepLength > 0) {
String keepStr= document.get(pos, keepLength);
replacementString= replacementString + keepStr;
setCursorPosition(replacementString.length() - keepLength);
}
} else
setCursorPosition(replacementString.length());
setReplacementString(replacementString);
if (pos < document.getLength() && document.getChar(pos) == ')') {
int currentLength= getReplacementLength();
if (replacementStringEndsWithParentheses)
setReplacementLength(currentLength + pos - offset);
else
setReplacementLength(currentLength + pos - offset + 1);
}
return false;
}
/*
* @see ICompletionProposalExtension#getContextInformationPosition()
* @since 3.4
*/
@Override
public int getContextInformationPosition() {
if (!fIsContextInformationComputed)
setContextInformation(computeContextInformation());
return fContextInformationPosition;
}
/*
* @see ICompletionProposal#getContextInformation()
* @since 3.4
*/
@Override
public final IContextInformation getContextInformation() {
if (!fIsContextInformationComputed)
setContextInformation(computeContextInformation());
return super.getContextInformation();
}
protected IContextInformation computeContextInformation() {
try {
fContextInformationPosition= getReplacementOffset() - 1;
CompletionProposal proposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
// no context information for METHOD_NAME_REF proposals (e.g. for static imports)
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=94654
if (hasParameters() && (getReplacementString().endsWith(")") || getReplacementString().length() == 0)) { //$NON-NLS-1$
ProposalContextInformation contextInformation= new ProposalContextInformation(proposal);
fContextInformationPosition= getReplacementOffset() + getCursorPosition();
if (fContextInformationPosition != 0 && proposal.getCompletion().length == 0)
contextInformation.setContextInformationPosition(fContextInformationPosition);
return contextInformation;
}
return null;
} finally {
fIsContextInformationComputed= true;
}
}
/**
* Returns <code>true</code> if the method being inserted has at least one parameter. Note
* that this does not say anything about whether the argument list should be inserted.
*
* @return <code>true</code> if the method has any parameters, <code>false</code> if it has no parameters
* @since 3.4
*/
private boolean hasParameters() {
CompletionProposal proposal= ((MemberProposalInfo)getProposalInfo()).fProposal;
return Signature.getParameterCount(proposal.getSignature()) > 0;
}
/*
* @see org.eclipse.jdt.internal.ui.text.java.AbstractJavaCompletionProposal#createLazyJavaTypeCompletionProposal(org.eclipse.jdt.core.CompletionProposal, org.eclipse.jdt.ui.text.java.JavaContentAssistInvocationContext)
* @since 3.5
*/
@Override
protected LazyJavaCompletionProposal createRequiredTypeCompletionProposal(CompletionProposal completionProposal, JavaContentAssistInvocationContext invocationContext) {
LazyJavaCompletionProposal proposal= super.createRequiredTypeCompletionProposal(completionProposal, invocationContext);
if (proposal instanceof LazyJavaTypeCompletionProposal)
((LazyJavaTypeCompletionProposal)proposal).setImportRewrite(fImportRewrite);
return proposal;
}
}