blob: 4a6b0db8308352cee84bbae06c0df4ada56a9145 [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
* Nikolay Metchev <nikolaymetchev@gmail.com> - Import static (Ctrl+Shift+M) creates imports for private methods - https://bugs.eclipse.org/409594
*******************************************************************************/
package org.eclipse.jdt.internal.corext.codemanipulation;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.resources.IWorkspaceRunnable;
import org.eclipse.text.edits.MultiTextEdit;
import org.eclipse.text.edits.ReplaceEdit;
import org.eclipse.text.edits.TextEdit;
import org.eclipse.jdt.core.Flags;
import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
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.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IBinding;
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.MarkerAnnotation;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Modifier;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.NameQualifiedType;
import org.eclipse.jdt.core.dom.NodeFinder;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.eclipse.jdt.core.dom.SimpleName;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
import org.eclipse.jdt.core.dom.rewrite.ImportRewrite.ImportRewriteContext;
import org.eclipse.jdt.core.manipulation.TypeKinds;
import org.eclipse.jdt.core.manipulation.TypeNameMatchCollector;
import org.eclipse.jdt.core.search.IJavaSearchConstants;
import org.eclipse.jdt.core.search.IJavaSearchScope;
import org.eclipse.jdt.core.search.SearchEngine;
import org.eclipse.jdt.core.search.SearchPattern;
import org.eclipse.jdt.core.search.TypeNameMatch;
import org.eclipse.jdt.internal.core.manipulation.dom.ASTResolving;
import org.eclipse.jdt.internal.core.manipulation.util.BasicElementLabels;
import org.eclipse.jdt.internal.corext.dom.ASTNodes;
import org.eclipse.jdt.internal.corext.util.JavaConventionsUtil;
import org.eclipse.jdt.internal.corext.util.JavaModelUtil;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.ui.SharedASTProvider;
import org.eclipse.jdt.internal.ui.JavaUIStatus;
/**
* Add imports to a compilation unit.
* The input is an array of full qualified type names. No elimination of unnecessary
* imports is done (use ImportStructure for this). Duplicates are not added.
* If the compilation unit is open in an editor, be sure to pass over its working copy.
*/
public class AddImportsOperation implements IWorkspaceRunnable {
public static interface IChooseImportQuery {
/**
* Selects an import from a list of choices.
* @param openChoices Array of found types
* @param containerName Name type to be imported
* @return Returns <code>null</code> to cancel the operation, or the
* selected imports.
*/
TypeNameMatch chooseImport(TypeNameMatch[] openChoices, String containerName);
}
private ICompilationUnit fCompilationUnit;
private final int fSelectionOffset;
private final int fSelectionLength;
private final IChooseImportQuery fQuery;
private IStatus fStatus;
private boolean fDoSave;
private final boolean fApply;
private MultiTextEdit fResultingEdit;
/**
* Generate import statements for the passed java elements
* Elements must be of type IType (-> single import) or IPackageFragment
* (on-demand-import). Other JavaElements are ignored
* @param cu The compilation unit
* @param selectionOffset Start of the current text selection
* @param selectionLength End of the current text selection
* @param query Query element to be used for UI interaction or <code>null</code> to not select anything
* when multiple possibilities are available
* @param save If set, the result will be saved
*/
public AddImportsOperation(ICompilationUnit cu, int selectionOffset, int selectionLength, IChooseImportQuery query, boolean save) {
this(cu, selectionOffset, selectionLength, query, save, true);
}
/**
* Generate import statements for the passed java elements
* Elements must be of type IType (-> single import) or IPackageFragment
* (on-demand-import). Other JavaElements are ignored
* @param cu The compilation unit
* @param selectionOffset Start of the current text selection
* @param selectionLength End of the current text selection
* @param query Query element to be used for UI interaction or <code>null</code> to not select anything
* when multiple possibilities are available
* @param save If set, the result will be saved
* @param apply If set, the resulting edit will be applied
*/
public AddImportsOperation(ICompilationUnit cu, int selectionOffset, int selectionLength, IChooseImportQuery query, boolean save, boolean apply) {
super();
Assert.isNotNull(cu);
fCompilationUnit= cu;
fSelectionOffset= selectionOffset;
fSelectionLength= selectionLength;
fQuery= query;
fStatus= Status.OK_STATUS;
fDoSave= save;
fApply= apply;
}
/**
* @return Returns the status.
*/
public IStatus getStatus() {
return fStatus;
}
/**
* Runs the operation.
*
* @param monitor The progress monitor
* @throws CoreException if accessing the CU or rewritting the imports fails
* @throws OperationCanceledException Runtime error thrown when operation is canceled.
*/
@Override
public void run(IProgressMonitor monitor) throws CoreException, OperationCanceledException {
if (monitor == null) {
monitor= new NullProgressMonitor();
}
try {
monitor.beginTask(CodeGenerationMessages.AddImportsOperation_description, 4);
CompilationUnit astRoot= SharedASTProvider.getAST(fCompilationUnit, SharedASTProvider.WAIT_YES, new SubProgressMonitor(monitor, 1));
if (astRoot == null)
throw new OperationCanceledException();
ImportRewrite importRewrite= StubUtility.createImportRewrite(astRoot, true);
MultiTextEdit res= new MultiTextEdit();
TextEdit edit= evaluateEdits(astRoot, importRewrite, fSelectionOffset, fSelectionLength, new SubProgressMonitor(monitor, 1));
if (edit == null) {
return;
}
res.addChild(edit);
TextEdit importsEdit= importRewrite.rewriteImports(new SubProgressMonitor(monitor, 1));
res.addChild(importsEdit);
fResultingEdit= res;
if (fApply) {
JavaModelUtil.applyEdit(fCompilationUnit, res, fDoSave, new SubProgressMonitor(monitor, 1));
}
} finally {
monitor.done();
}
}
/**
* Returns the resulting edit
*
* @return the resulting edit
*/
public MultiTextEdit getResultingEdit() {
return fResultingEdit;
}
private TextEdit evaluateEdits(CompilationUnit root, ImportRewrite importRewrite, int offset, int length, IProgressMonitor monitor) throws JavaModelException {
SimpleName nameNode= null;
if (root != null) { // got an AST
ASTNode node= NodeFinder.perform(root, offset, length);
if (node instanceof MarkerAnnotation) {
node= ((Annotation) node).getTypeName();
}
if (node instanceof QualifiedName) {
nameNode= ((QualifiedName) node).getName();
} else if (node instanceof SimpleName) {
nameNode= (SimpleName) node;
}
}
String name, simpleName, containerName;
int qualifierStart;
int simpleNameStart;
if (nameNode != null) {
simpleName= nameNode.getIdentifier();
simpleNameStart= nameNode.getStartPosition();
if (nameNode.getLocationInParent() == QualifiedName.NAME_PROPERTY) {
Name qualifier= ((QualifiedName) nameNode.getParent()).getQualifier();
containerName= qualifier.getFullyQualifiedName();
name= JavaModelUtil.concatenateName(containerName, simpleName);
qualifierStart= qualifier.getStartPosition();
} else if (nameNode.getLocationInParent() == NameQualifiedType.NAME_PROPERTY) {
NameQualifiedType nameQualifiedType= (NameQualifiedType) nameNode.getParent();
Name qualifier= nameQualifiedType.getQualifier();
containerName= qualifier.getFullyQualifiedName();
name= JavaModelUtil.concatenateName(containerName, simpleName);
qualifierStart= qualifier.getStartPosition();
List<Annotation> annotations= nameQualifiedType.annotations();
if (!annotations.isEmpty()) { // don't remove annotations
simpleNameStart= annotations.get(0).getStartPosition();
}
} else if (nameNode.getLocationInParent() == MethodInvocation.NAME_PROPERTY) {
ASTNode qualifier= ((MethodInvocation) nameNode.getParent()).getExpression();
if (qualifier instanceof Name) {
containerName= ASTNodes.asString(qualifier);
name= JavaModelUtil.concatenateName(containerName, simpleName);
qualifierStart= qualifier.getStartPosition();
} else {
return null;
}
} else {
containerName= ""; //$NON-NLS-1$
name= simpleName;
qualifierStart= simpleNameStart;
}
IBinding binding= nameNode.resolveBinding();
if (binding != null && !binding.isRecovered()) {
if (binding instanceof ITypeBinding) {
ITypeBinding typeBinding= ((ITypeBinding) binding).getTypeDeclaration();
String qualifiedBindingName= typeBinding.getQualifiedName();
if (containerName.length() > 0 && !qualifiedBindingName.equals(name)) {
return null;
}
ImportRewriteContext context= new ContextSensitiveImportRewriteContext(root, qualifierStart, importRewrite);
String res= importRewrite.addImport(typeBinding, context);
if (containerName.length() > 0 && !res.equals(simpleName)) {
// adding import failed
fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_importclash, null);
return null;
}
if (containerName.length() == 0 && res.equals(simpleName)) {
// no change necessary
return null;
}
return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, new String());
} else if (JavaModelUtil.is50OrHigher(fCompilationUnit.getJavaProject()) && (binding instanceof IVariableBinding || binding instanceof IMethodBinding)) {
boolean isField= binding instanceof IVariableBinding;
ITypeBinding declaringClass= isField ? ((IVariableBinding) binding).getDeclaringClass() : ((IMethodBinding) binding).getDeclaringClass();
if (declaringClass == null) {
return null; // variableBinding.getDeclaringClass() is null for array.length
}
if (Modifier.isStatic(binding.getModifiers())) {
if (containerName.length() > 0) {
if (containerName.equals(declaringClass.getName()) || containerName.equals(declaringClass.getQualifiedName()) ) {
ASTNode node= nameNode.getParent();
boolean isDirectlyAccessible= false;
while (node != null) {
if (isTypeDeclarationSubTypeCompatible(node, declaringClass)) {
isDirectlyAccessible= true;
break;
}
node= node.getParent();
}
if (!isDirectlyAccessible) {
if (Modifier.isPrivate(declaringClass.getModifiers())) {
fStatus= JavaUIStatus.createError(IStatus.ERROR,
Messages.format(CodeGenerationMessages.AddImportsOperation_error_not_visible_class, BasicElementLabels.getJavaElementName(declaringClass.getName())),
null);
return null;
}
String res= importRewrite.addStaticImport(declaringClass.getQualifiedName(), binding.getName(), isField);
if (!res.equals(simpleName)) {
// adding import failed
return null;
}
}
return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
}
}
}
return null; // no static imports for packages
} else {
return null;
}
}
if (binding != null && binding.getKind() != IBinding.TYPE) {
// recovered binding
return null;
}
} else {
IBuffer buffer= fCompilationUnit.getBuffer();
qualifierStart= getNameStart(buffer, offset);
int nameEnd= getNameEnd(buffer, offset + length);
int len= nameEnd - qualifierStart;
name= buffer.getText(qualifierStart, len).trim();
if (name.length() == 0) {
return null;
}
simpleName= Signature.getSimpleName(name);
containerName= Signature.getQualifier(name);
IJavaProject javaProject= fCompilationUnit.getJavaProject();
if (simpleName.length() == 0 || JavaConventionsUtil.validateJavaTypeName(simpleName, javaProject).matches(IStatus.ERROR)
|| (containerName.length() > 0 && JavaConventionsUtil.validateJavaTypeName(containerName, javaProject).matches(IStatus.ERROR))) {
fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_invalid_selection, null);
return null;
}
simpleNameStart= getSimpleNameStart(buffer, qualifierStart, containerName);
int res= importRewrite.getDefaultImportRewriteContext().findInContext(containerName, simpleName, ImportRewriteContext.KIND_TYPE);
if (res == ImportRewriteContext.RES_NAME_CONFLICT) {
fStatus= JavaUIStatus.createError(IStatus.ERROR, CodeGenerationMessages.AddImportsOperation_error_importclash, null);
return null;
} else if (res == ImportRewriteContext.RES_NAME_FOUND) {
return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
}
}
IJavaSearchScope searchScope= SearchEngine.createJavaSearchScope(new IJavaElement[] { fCompilationUnit.getJavaProject() });
TypeNameMatch[] types= findAllTypes(simpleName, searchScope, nameNode, new SubProgressMonitor(monitor, 1));
if (types.length == 0) {
fStatus= JavaUIStatus.createError(IStatus.ERROR, Messages.format(CodeGenerationMessages.AddImportsOperation_error_notresolved_message, BasicElementLabels.getJavaElementName(simpleName)), null);
return null;
}
if (monitor.isCanceled()) {
throw new OperationCanceledException();
}
TypeNameMatch chosen;
if (types.length > 1 && fQuery != null) {
chosen= fQuery.chooseImport(types, containerName);
if (chosen == null) {
throw new OperationCanceledException();
}
} else {
chosen= types[0];
}
ImportRewriteContext context= root == null ? null : new ContextSensitiveImportRewriteContext(root, simpleNameStart, importRewrite);
importRewrite.addImport(chosen.getFullyQualifiedName(), context);
return new ReplaceEdit(qualifierStart, simpleNameStart - qualifierStart, ""); //$NON-NLS-1$
}
private boolean isTypeDeclarationSubTypeCompatible(ASTNode typeDeclaration, ITypeBinding supertype) {
if (typeDeclaration instanceof AbstractTypeDeclaration) {
ITypeBinding binding= ((AbstractTypeDeclaration) typeDeclaration).resolveBinding();
return binding != null && binding.isSubTypeCompatible(supertype);
} else if (typeDeclaration instanceof AnonymousClassDeclaration) {
ITypeBinding binding= ((AnonymousClassDeclaration) typeDeclaration).resolveBinding();
return binding != null && binding.isSubTypeCompatible(supertype);
} else {
return false;
}
}
private int getNameStart(IBuffer buffer, int pos) {
while (pos > 0) {
char ch= buffer.getChar(pos - 1);
if (!Character.isJavaIdentifierPart(ch) && ch != '.') {
return pos;
}
pos--;
}
return pos;
}
private int getNameEnd(IBuffer doc, int pos) {
if (pos > 0) {
if (Character.isWhitespace(doc.getChar(pos - 1))) {
return pos;
}
}
int len= doc.getLength();
while (pos < len) {
char ch= doc.getChar(pos);
if (!Character.isJavaIdentifierPart(ch)) {
return pos;
}
pos++;
}
return pos;
}
private int getSimpleNameStart(IBuffer buffer, int nameStart, String containerName) {
int containerLen= containerName.length();
int docLen= buffer.getLength();
if (containerLen > 0 && nameStart + containerLen + 1 < docLen) {
for (int k= 0; k < containerLen; k++) {
if (buffer.getChar(nameStart + k) != containerName.charAt(k)) {
return nameStart;
}
}
if (buffer.getChar(nameStart + containerLen) == '.') {
return nameStart + containerLen + 1;
}
}
return nameStart;
}
private int getSearchForConstant(int typeKinds) {
final int CLASSES= TypeKinds.CLASSES;
final int INTERFACES= TypeKinds.INTERFACES;
final int ENUMS= TypeKinds.ENUMS;
final int ANNOTATIONS= TypeKinds.ANNOTATIONS;
switch (typeKinds & (CLASSES | INTERFACES | ENUMS | ANNOTATIONS)) {
case CLASSES: return IJavaSearchConstants.CLASS;
case INTERFACES: return IJavaSearchConstants.INTERFACE;
case ENUMS: return IJavaSearchConstants.ENUM;
case ANNOTATIONS: return IJavaSearchConstants.ANNOTATION_TYPE;
case CLASSES | INTERFACES: return IJavaSearchConstants.CLASS_AND_INTERFACE;
case CLASSES | ENUMS: return IJavaSearchConstants.CLASS_AND_ENUM;
default: return IJavaSearchConstants.TYPE;
}
}
/*
* Finds a type by the simple name.
*/
private TypeNameMatch[] findAllTypes(String simpleTypeName, IJavaSearchScope searchScope, SimpleName nameNode, IProgressMonitor monitor) throws JavaModelException {
boolean is50OrHigher= JavaModelUtil.is50OrHigher(fCompilationUnit.getJavaProject());
int typeKinds= TypeKinds.ALL_TYPES;
if (nameNode != null) {
typeKinds= ASTResolving.getPossibleTypeKinds(nameNode, is50OrHigher);
}
ArrayList<TypeNameMatch> typeInfos= new ArrayList<>();
TypeNameMatchCollector requestor= new TypeNameMatchCollector(typeInfos);
int matchMode= SearchPattern.R_EXACT_MATCH | SearchPattern.R_CASE_SENSITIVE;
new SearchEngine().searchAllTypeNames(null, matchMode, simpleTypeName.toCharArray(), matchMode, getSearchForConstant(typeKinds), searchScope, requestor, IJavaSearchConstants.WAIT_UNTIL_READY_TO_SEARCH, monitor);
ArrayList<TypeNameMatch> typeRefsFound= new ArrayList<>(typeInfos.size());
for (int i= 0, len= typeInfos.size(); i < len; i++) {
TypeNameMatch curr= typeInfos.get(i);
if (curr.getPackageName().length() > 0) { // do not suggest imports from the default package
if (isOfKind(curr, typeKinds, is50OrHigher) && isVisible(curr)) {
typeRefsFound.add(curr);
}
}
}
return typeRefsFound.toArray(new TypeNameMatch[typeRefsFound.size()]);
}
private boolean isOfKind(TypeNameMatch curr, int typeKinds, boolean is50OrHigher) {
int flags= curr.getModifiers();
if (Flags.isAnnotation(flags)) {
return is50OrHigher && (typeKinds & TypeKinds.ANNOTATIONS) != 0;
}
if (Flags.isEnum(flags)) {
return is50OrHigher && (typeKinds & TypeKinds.ENUMS) != 0;
}
if (Flags.isInterface(flags)) {
return (typeKinds & TypeKinds.INTERFACES) != 0;
}
return (typeKinds & TypeKinds.CLASSES) != 0;
}
private boolean isVisible(TypeNameMatch curr) {
int flags= curr.getModifiers();
if (Flags.isPrivate(flags)) {
return false;
}
if (Flags.isPublic(flags) || Flags.isProtected(flags)) {
return true;
}
return curr.getPackageName().equals(fCompilationUnit.getParent().getElementName());
}
/**
* @return Returns the scheduling rule for this operation
*/
public ISchedulingRule getScheduleRule() {
return fCompilationUnit.getJavaProject().getResource();
}
}