| /******************************************************************************* |
| * Copyright (c) 2000, 2018 IBM Corporation and others. |
| * |
| * This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License 2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * 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.SharedASTProviderCore; |
| 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.StubUtility; |
| 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.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 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= SharedASTProviderCore.getAST(fCompilationUnit, SharedASTProviderCore.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, ""); //$NON-NLS-1$ |
| } 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(); |
| } |
| |
| } |