1 /*******************************************************************************
2  * Copyright (c) 2000, 2018 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Red Hat Inc. - copied to jdt.core.manipulation
14  *******************************************************************************/
15 package org.eclipse.jdt.internal.corext.refactoring.structure;
16 
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.HashMap;
20 import java.util.HashSet;
21 import java.util.Iterator;
22 import java.util.List;
23 import java.util.Set;
24 
25 import org.eclipse.jdt.core.IJavaProject;
26 import org.eclipse.jdt.core.dom.ASTNode;
27 import org.eclipse.jdt.core.dom.ASTVisitor;
28 import org.eclipse.jdt.core.dom.CompilationUnit;
29 import org.eclipse.jdt.core.dom.IBinding;
30 import org.eclipse.jdt.core.dom.IMethodBinding;
31 import org.eclipse.jdt.core.dom.ITypeBinding;
32 import org.eclipse.jdt.core.dom.IVariableBinding;
33 import org.eclipse.jdt.core.dom.ImportDeclaration;
34 import org.eclipse.jdt.core.dom.Name;
35 import org.eclipse.jdt.core.dom.NameQualifiedType;
36 import org.eclipse.jdt.core.dom.QualifiedName;
37 import org.eclipse.jdt.core.dom.QualifiedType;
38 import org.eclipse.jdt.core.dom.SimpleName;
39 import org.eclipse.jdt.core.dom.Type;
40 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
41 import org.eclipse.jdt.core.manipulation.ImportReferencesCollector;
42 
43 import org.eclipse.jdt.internal.corext.dom.Bindings;
44 
45 /**
46  * Removes imports that are no longer required.
47  * <p>
48  * {@link #registerRemovedNode(ASTNode)} registers nodes that got removed from the AST.
49  * Do not register nodes that are moved to a different place in the same AST.
50  * <p>
51  * If a node is removed but some parts of it are moved to a different place in the same AST,
52  * then use {@link #registerRetainedNode(ASTNode)} to keep imports for the retained nodes.
53  * <p>
54  * Additional imports that will be added to the AST need to be registered with one of the
55  * {@code register*Import*(..)} methods. Such imports have typically been created with
56  * {@link ImportRewrite#addImport(ITypeBinding)} etc.
57  */
58 public class ImportRemover {
59 
60 	private static class StaticImportData {
61 
62 		private boolean fField;
63 
64 		private String fMember;
65 
66 		private String fQualifier;
67 
StaticImportData(String qualifier, String member, boolean field)68 		private StaticImportData(String qualifier, String member, boolean field) {
69 			fQualifier= qualifier;
70 			fMember= member;
71 			fField= field;
72 		}
73 	}
74 
75 	private final String PROPERTY_KEY= String.valueOf(System.currentTimeMillis());
76 	private final String REMOVED= "removed"; //$NON-NLS-1$
77 	private final String RETAINED= "retained"; //$NON-NLS-1$
78 
79 	private Set<String> fAddedImports= new HashSet<>();
80 
81 	private Set<StaticImportData> fAddedStaticImports= new HashSet<>();
82 
83 	private final IJavaProject fProject;
84 
85 	private boolean fHasRemovedNodes;
86 
87 	private List<ImportDeclaration> fInlinedStaticImports= new ArrayList<>();
88 
89 	private final CompilationUnit fRoot;
90 
ImportRemover(IJavaProject project, CompilationUnit root)91 	public ImportRemover(IJavaProject project, CompilationUnit root) {
92 		fProject= project;
93 		fRoot= root;
94 	}
95 
divideTypeRefs(List<SimpleName> importNames, List<SimpleName> staticNames, List<SimpleName> removedRefs, List<SimpleName> unremovedRefs)96 	private void divideTypeRefs(List<SimpleName> importNames, List<SimpleName> staticNames, List<SimpleName> removedRefs, List<SimpleName> unremovedRefs) {
97 		final List<int[]> removedStartsEnds= new ArrayList<>();
98 		fRoot.accept(new ASTVisitor(true) {
99 			int fRemovingStart= -1;
100 			@Override
101 			public void preVisit(ASTNode node) {
102 				Object property= node.getProperty(PROPERTY_KEY);
103 				if (property == REMOVED) {
104 					if (fRemovingStart == -1) {
105 						fRemovingStart= node.getStartPosition();
106 					} else {
107 						/*
108 						 * Bug in client code: REMOVED node should not be nested inside another REMOVED node without
109 						 * an intermediate RETAINED node.
110 						 * Drop REMOVED property to prevent problems later (premature end of REMOVED section).
111 						 */
112 						node.setProperty(PROPERTY_KEY, null);
113 					}
114 				} else if (property == RETAINED) {
115 					if (fRemovingStart != -1) {
116 						removedStartsEnds.add(new int[] { fRemovingStart, node.getStartPosition() });
117 						fRemovingStart= -1;
118 					} else {
119 						/*
120 						 * Bug in client code: RETAINED node should not be nested inside another RETAINED node without
121 						 * an intermediate REMOVED node and must have an enclosing REMOVED node.
122 						 * Drop RETAINED property to prevent problems later (premature restart of REMOVED section).
123 						 */
124 						node.setProperty(PROPERTY_KEY, null);
125 					}
126 				}
127 				super.preVisit(node);
128 			}
129 			@Override
130 			public void postVisit(ASTNode node) {
131 				Object property= node.getProperty(PROPERTY_KEY);
132 				if (property == RETAINED) {
133 					int end= node.getStartPosition() + node.getLength();
134 					fRemovingStart= end;
135 				} else if (property == REMOVED) {
136 					if (fRemovingStart != -1) {
137 						int end= node.getStartPosition() + node.getLength();
138 						removedStartsEnds.add(new int[] { fRemovingStart, end });
139 						fRemovingStart= -1;
140 					}
141 				}
142 				super.postVisit(node);
143 			}
144 		});
145 
146 		for (SimpleName name : importNames) {
147 			if (isInRemoved(name, removedStartsEnds))
148 				removedRefs.add(name);
149 			else
150 				unremovedRefs.add(name);
151 		}
152 		for (SimpleName name : staticNames) {
153 			if (isInRemoved(name, removedStartsEnds))
154 				removedRefs.add(name);
155 			else
156 				unremovedRefs.add(name);
157 		}
158 		for (ImportDeclaration importDecl : fInlinedStaticImports) {
159 			Name name= importDecl.getName();
160 			if (name instanceof QualifiedName)
161 				name= ((QualifiedName) name).getName();
162 			removedRefs.add((SimpleName) name);
163 		}
164 	}
165 
isInRemoved(SimpleName ref, List<int[]> removedStartsEnds)166 	private boolean isInRemoved(SimpleName ref, List<int[]> removedStartsEnds) {
167 		int start= ref.getStartPosition();
168 		int end= start + ref.getLength();
169 		for (int[] removedStartsEnd : removedStartsEnds) {
170 			if (start >= removedStartsEnd[0] && end <= removedStartsEnd[1]) {
171 				return true;
172 			}
173 		}
174 		return false;
175 	}
176 
getImportsToRemove()177 	public IBinding[] getImportsToRemove() {
178 		ArrayList<SimpleName> importNames= new ArrayList<>();
179 		ArrayList<SimpleName> staticNames= new ArrayList<>();
180 
181 		ImportReferencesCollector.collect(fRoot, fProject, null, importNames, staticNames);
182 
183 		List<SimpleName> removedRefs= new ArrayList<>();
184 		List<SimpleName> unremovedRefs= new ArrayList<>();
185 		divideTypeRefs(importNames, staticNames, removedRefs, unremovedRefs);
186 		if (removedRefs.isEmpty())
187 			return new IBinding[0];
188 
189 		HashMap<String, IBinding> potentialRemoves= getPotentialRemoves(removedRefs);
190 		for (SimpleName name : unremovedRefs) {
191 			potentialRemoves.remove(name.getIdentifier());
192 		}
193 
194 		Collection<IBinding> importsToRemove= potentialRemoves.values();
195 		return importsToRemove.toArray(new IBinding[importsToRemove.size()]);
196 	}
197 
getPotentialRemoves(List<SimpleName> removedRefs)198 	private HashMap<String, IBinding> getPotentialRemoves(List<SimpleName> removedRefs) {
199 		HashMap<String, IBinding>potentialRemoves= new HashMap<>();
200 		for (SimpleName name : removedRefs) {
201 			if (fAddedImports.contains(name.getIdentifier()) || hasAddedStaticImport(name))
202 				continue;
203 			IBinding binding= name.resolveBinding();
204 			if (binding != null)
205 				potentialRemoves.put(name.getIdentifier(), binding);
206 		}
207 		return potentialRemoves;
208 	}
209 
hasAddedStaticImport(SimpleName name)210 	private boolean hasAddedStaticImport(SimpleName name) {
211 		IBinding binding= name.resolveBinding();
212 		if (binding instanceof IVariableBinding) {
213 			IVariableBinding variable= (IVariableBinding) binding;
214 			return hasAddedStaticImport(variable.getDeclaringClass().getQualifiedName(), variable.getName(), true);
215 		} else if (binding instanceof IMethodBinding) {
216 			IMethodBinding method= (IMethodBinding) binding;
217 			return hasAddedStaticImport(method.getDeclaringClass().getQualifiedName(), method.getName(), false);
218 		}
219 		return false;
220 	}
221 
hasAddedStaticImport(String qualifier, String member, boolean field)222 	private boolean hasAddedStaticImport(String qualifier, String member, boolean field) {
223 		StaticImportData data= null;
224 		for (final Iterator<StaticImportData> iterator= fAddedStaticImports.iterator(); iterator.hasNext();) {
225 			data= iterator.next();
226 			if (data.fQualifier.equals(qualifier) && data.fMember.equals(member) && data.fField == field)
227 				return true;
228 		}
229 		return false;
230 	}
231 
hasRemovedNodes()232 	public boolean hasRemovedNodes() {
233 		return fHasRemovedNodes || !fInlinedStaticImports.isEmpty();
234 	}
235 
registerAddedImport(String typeName)236 	public void registerAddedImport(String typeName) {
237 		int dot= typeName.lastIndexOf('.');
238 		if (dot == -1)
239 			fAddedImports.add(typeName);
240 		else
241 			fAddedImports.add(typeName.substring(dot + 1));
242 	}
243 
registerAddedImports(Type newTypeNode)244 	public void registerAddedImports(Type newTypeNode) {
245 		newTypeNode.accept(new ASTVisitor(true) {
246 
247 			private void addName(SimpleName name) {
248 				fAddedImports.add(name.getIdentifier());
249 			}
250 
251 			@Override
252 			public boolean visit(NameQualifiedType node) {
253 				addName(node.getName());
254 				return false;
255 			}
256 
257 			@Override
258 			public boolean visit(QualifiedName node) {
259 				addName(node.getName());
260 				return false;
261 			}
262 
263 			@Override
264 			public boolean visit(QualifiedType node) {
265 				addName(node.getName());
266 				return false;
267 			}
268 
269 			@Override
270 			public boolean visit(SimpleName node) {
271 				addName(node);
272 				return false;
273 			}
274 		});
275 	}
276 
registerAddedStaticImport(String qualifier, String member, boolean field)277 	public void registerAddedStaticImport(String qualifier, String member, boolean field) {
278 		fAddedStaticImports.add(new StaticImportData(qualifier, member, field));
279 	}
280 
registerAddedStaticImport(IBinding binding)281 	public void registerAddedStaticImport(IBinding binding) {
282 		if (binding instanceof IVariableBinding) {
283 			ITypeBinding declaringType= ((IVariableBinding) binding).getDeclaringClass();
284 			fAddedStaticImports.add(new StaticImportData(Bindings.getRawQualifiedName(declaringType), binding.getName(), true));
285 
286 		} else if (binding instanceof IMethodBinding) {
287 			ITypeBinding declaringType= ((IMethodBinding) binding).getDeclaringClass();
288 			fAddedStaticImports.add(new StaticImportData(Bindings.getRawQualifiedName(declaringType), binding.getName(), false));
289 
290 		} else {
291 			throw new IllegalArgumentException(binding.toString());
292 		}
293 	}
294 
registerRemovedNode(ASTNode removed)295 	public void registerRemovedNode(ASTNode removed) {
296 		fHasRemovedNodes= true;
297 		removed.setProperty(PROPERTY_KEY, REMOVED);
298 	}
299 
registerRetainedNode(ASTNode retained)300 	public void registerRetainedNode(ASTNode retained) {
301 		retained.setProperty(PROPERTY_KEY, RETAINED);
302 	}
303 
applyRemoves(ImportRewrite importRewrite)304 	public void applyRemoves(ImportRewrite importRewrite) {
305 		for (IBinding b : getImportsToRemove()) {
306 			if (b instanceof ITypeBinding) {
307 				ITypeBinding typeBinding= (ITypeBinding) b;
308 				importRewrite.removeImport(typeBinding.getTypeDeclaration().getQualifiedName());
309 			} else if (b instanceof IMethodBinding) {
310 				IMethodBinding binding= (IMethodBinding) b;
311 				importRewrite.removeStaticImport(binding.getDeclaringClass().getQualifiedName() + '.' + binding.getName());
312 			} else if (b instanceof IVariableBinding) {
313 				IVariableBinding binding= (IVariableBinding) b;
314 				importRewrite.removeStaticImport(binding.getDeclaringClass().getQualifiedName() + '.' + binding.getName());
315 			}
316 		}
317 	}
318 
registerInlinedStaticImport(ImportDeclaration importDecl)319 	public void registerInlinedStaticImport(ImportDeclaration importDecl) {
320 		fInlinedStaticImports.add(importDecl);
321 	}
322 }
323