Added
Link Here
|
1 |
/* |
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
3 |
* |
4 |
* Copyright 2009 Sun Microsystems, Inc. All rights reserved. |
5 |
* |
6 |
* The contents of this file are subject to the terms of either the GNU |
7 |
* General Public License Version 2 only ("GPL") or the Common |
8 |
* Development and Distribution License("CDDL") (collectively, the |
9 |
* "License"). You may not use this file except in compliance with the |
10 |
* License. You can obtain a copy of the License at |
11 |
* http://www.netbeans.org/cddl-gplv2.html |
12 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the |
13 |
* specific language governing permissions and limitations under the |
14 |
* License. When distributing the software, include this License Header |
15 |
* Notice in each file and include the License file at |
16 |
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this |
17 |
* particular file as subject to the "Classpath" exception as provided |
18 |
* by Sun in the GPL Version 2 section of the License file that |
19 |
* accompanied this code. If applicable, add the following below the |
20 |
* License Header, with the fields enclosed by brackets [] replaced by |
21 |
* your own identifying information: |
22 |
* "Portions Copyrighted [year] [name of copyright owner]" |
23 |
* |
24 |
* Contributor(s): |
25 |
* |
26 |
* Portions Copyrighted 2009 Sun Microsystems, Inc. |
27 |
*/ |
28 |
package org.netbeans.modules.java.hints; |
29 |
|
30 |
import com.sun.source.tree.CompilationUnitTree; |
31 |
import com.sun.source.tree.ImportTree; |
32 |
import com.sun.source.tree.Tree.Kind; |
33 |
import com.sun.source.util.TreePath; |
34 |
import java.util.Collections; |
35 |
import java.util.EnumSet; |
36 |
import java.util.List; |
37 |
import java.util.Set; |
38 |
import java.util.concurrent.atomic.AtomicBoolean; |
39 |
import javax.lang.model.element.Element; |
40 |
import javax.lang.model.element.ElementKind; |
41 |
import javax.lang.model.element.Modifier; |
42 |
import javax.lang.model.element.TypeElement; |
43 |
import javax.lang.model.type.TypeMirror; |
44 |
import javax.lang.model.util.Types; |
45 |
import org.netbeans.api.java.queries.SourceLevelQuery; |
46 |
import org.netbeans.api.java.source.CompilationInfo; |
47 |
import org.netbeans.api.java.source.ElementUtilities; |
48 |
import org.netbeans.api.java.source.JavaSource; |
49 |
import org.netbeans.api.java.source.JavaSource.Phase; |
50 |
import org.netbeans.api.java.source.SourceUtils; |
51 |
import org.netbeans.api.java.source.Task; |
52 |
import org.netbeans.api.java.source.TreeMaker; |
53 |
import org.netbeans.api.java.source.TreePathHandle; |
54 |
import org.netbeans.api.java.source.WorkingCopy; |
55 |
import org.netbeans.modules.java.hints.spi.AbstractHint; |
56 |
import org.netbeans.spi.editor.hints.ChangeInfo; |
57 |
import org.netbeans.spi.editor.hints.ErrorDescription; |
58 |
import org.netbeans.spi.editor.hints.ErrorDescriptionFactory; |
59 |
import org.netbeans.spi.editor.hints.Fix; |
60 |
import org.openide.util.NbBundle; |
61 |
import static org.netbeans.modules.editor.java.Utilities.getElementName; |
62 |
|
63 |
/** |
64 |
* Hint offering to convert a qualified static method into a static import. e.g. |
65 |
* <code>Math.abs(-1)</code> -> <code>abs(-1)</code>. |
66 |
* <p> |
67 |
* Future versions might support other member types. |
68 |
* |
69 |
* @author Sam Halliday |
70 |
* @see <a href="http://www.netbeans.org/issues/show_bug.cgi?id=89258">RFE 89258</a> |
71 |
* @see <a href="http://java.sun.com/j2se/1.5.0/docs/guide/language/static-import.html>Static Imports</a> |
72 |
*/ |
73 |
public class StaticImport extends AbstractHint { |
74 |
|
75 |
private final AtomicBoolean cancel = new AtomicBoolean(); |
76 |
|
77 |
public StaticImport() { |
78 |
super(true, false, HintSeverity.CURRENT_LINE_WARNING); |
79 |
} |
80 |
|
81 |
@Override |
82 |
public String getDescription() { |
83 |
return NbBundle.getMessage(StaticImport.class, "DSC_StaticImport"); |
84 |
} |
85 |
|
86 |
public Set<Kind> getTreeKinds() { |
87 |
return EnumSet.of(Kind.MEMBER_SELECT); |
88 |
} |
89 |
|
90 |
public List<ErrorDescription> run(CompilationInfo info, TreePath treePath) { |
91 |
if (treePath == null || treePath.getLeaf().getKind() != Kind.MEMBER_SELECT) { |
92 |
return null; |
93 |
} |
94 |
cancel.set(false); |
95 |
TreePath mitp = treePath.getParentPath(); |
96 |
if (mitp == null || mitp.getLeaf().getKind() != Kind.METHOD_INVOCATION) { |
97 |
return null; |
98 |
} |
99 |
Element e = info.getTrees().getElement(treePath); |
100 |
if (e == null || !e.getModifiers().contains(Modifier.STATIC)) { |
101 |
return null; |
102 |
} |
103 |
if (!supportsStaticImports(info)) { |
104 |
return null; |
105 |
} |
106 |
Element enclosingEl = e.getEnclosingElement(); |
107 |
if (enclosingEl == null) { |
108 |
return null; |
109 |
} |
110 |
String sn = e.getSimpleName().toString(); |
111 |
if (!isValidStaticMethod(info, getElementName(enclosingEl, true).toString(), sn)) { |
112 |
return null; |
113 |
} |
114 |
Element klass = info.getTrees().getElement(getContainingClass(treePath)); |
115 |
String fqn = null; |
116 |
String fqn1 = getMethodFqn(e); |
117 |
if (!isSubTypeOrInnerOfSubType(info, klass, enclosingEl) && !isStaticallyImported(info, fqn1)) { |
118 |
if (hasMethodNameClash(info, klass, sn) || hasStaticImportSimpleNameClash(info, sn)) { |
119 |
return null; |
120 |
} |
121 |
fqn = fqn1; |
122 |
} |
123 |
List<Fix> fixes = Collections.<Fix>singletonList(new FixImpl(TreePathHandle.create(treePath, info), fqn, sn)); |
124 |
String desc = NbBundle.getMessage(AddOverrideAnnotation.class, "ERR_StaticImport"); |
125 |
int start = (int) info.getTrees().getSourcePositions().getStartPosition(info.getCompilationUnit(), treePath.getLeaf()); |
126 |
int end = (int) info.getTrees().getSourcePositions().getEndPosition(info.getCompilationUnit(), treePath.getLeaf()); |
127 |
ErrorDescription ed = ErrorDescriptionFactory.createErrorDescription(getSeverity().toEditorSeverity(), desc, fixes, info.getFileObject(), start, end); |
128 |
if (cancel.get()) { |
129 |
return null; |
130 |
} |
131 |
return Collections.singletonList(ed); |
132 |
} |
133 |
|
134 |
public String getId() { |
135 |
return StaticImport.class.getName(); |
136 |
} |
137 |
|
138 |
public String getDisplayName() { |
139 |
return NbBundle.getMessage(StaticImport.class, "DN_StaticImport"); |
140 |
} |
141 |
|
142 |
public void cancel() { |
143 |
cancel.set(true); |
144 |
} |
145 |
|
146 |
/** |
147 |
* @param info |
148 |
* @return true if the source level supports the static import language feature |
149 |
*/ |
150 |
public static boolean supportsStaticImports(CompilationInfo info) { |
151 |
String level = SourceLevelQuery.getSourceLevel(info.getFileObject()); |
152 |
if (level == null) { |
153 |
return false; |
154 |
} |
155 |
try { |
156 |
double dLevel = Double.valueOf(level); |
157 |
if (dLevel < 1.5) { |
158 |
return false; |
159 |
} |
160 |
return true; |
161 |
} catch (NumberFormatException e) { |
162 |
return false; |
163 |
} |
164 |
} |
165 |
|
166 |
public static final class FixImpl implements Fix, Task<WorkingCopy> { |
167 |
|
168 |
private final TreePathHandle handle; |
169 |
private final String fqn; |
170 |
private final String sn; |
171 |
|
172 |
/** |
173 |
* @param handle to the MEMBER_SELECT |
174 |
* @param fqn to static import, or null to not perform any imports |
175 |
* @param sn simple name |
176 |
*/ |
177 |
public FixImpl(TreePathHandle handle, String fqn, String sn) { |
178 |
this.handle = handle; |
179 |
this.fqn = fqn; |
180 |
this.sn = sn; |
181 |
} |
182 |
|
183 |
public String getText() { |
184 |
if (fqn == null) { |
185 |
return NbBundle.getMessage(StaticImport.class, "HINT_StaticImport", sn); |
186 |
} else { |
187 |
return NbBundle.getMessage(StaticImport.class, "HINT_StaticImport2", fqn); |
188 |
} |
189 |
} |
190 |
|
191 |
public ChangeInfo implement() throws Exception { |
192 |
JavaSource js = JavaSource.forFileObject(handle.getFileObject()); |
193 |
js.runModificationTask(this).commit(); |
194 |
return null; |
195 |
} |
196 |
|
197 |
public void run(WorkingCopy copy) throws Exception { |
198 |
if (copy.toPhase(Phase.RESOLVED).compareTo(Phase.RESOLVED) < 0) { |
199 |
return; |
200 |
} |
201 |
TreePath treePath = handle.resolve(copy); |
202 |
if (treePath == null || treePath.getLeaf().getKind() != Kind.MEMBER_SELECT) { |
203 |
return; |
204 |
} |
205 |
TreePath mitp = treePath.getParentPath(); |
206 |
if (mitp == null || mitp.getLeaf().getKind() != Kind.METHOD_INVOCATION) { |
207 |
return; |
208 |
} |
209 |
Element e = copy.getTrees().getElement(treePath); |
210 |
if (e == null || !e.getModifiers().contains(Modifier.STATIC)) { |
211 |
return; |
212 |
} |
213 |
TreeMaker make = copy.getTreeMaker(); |
214 |
copy.rewrite(treePath.getLeaf(), make.Identifier(sn)); |
215 |
if (fqn == null) { |
216 |
return; |
217 |
} |
218 |
CompilationUnitTree cut = copy.getCompilationUnit(); |
219 |
CompilationUnitTree nue = SourceUtils.addStaticImports(cut, Collections.singletonList(fqn), make); |
220 |
copy.rewrite(cut, nue); |
221 |
} |
222 |
} |
223 |
|
224 |
// returns true if a METHOD is enclosed in element with simple name sn |
225 |
private static boolean hasMethodWithSimpleName(CompilationInfo info, Element element, final String sn) { |
226 |
Iterable<? extends Element> members = |
227 |
info.getElementUtilities().getMembers(element.asType(), new ElementUtilities.ElementAcceptor() { |
228 |
|
229 |
public boolean accept(Element e, TypeMirror type) { |
230 |
if (e.getKind() == ElementKind.METHOD && e.getSimpleName().toString().equals(sn)) { |
231 |
return true; |
232 |
} |
233 |
return false; |
234 |
} |
235 |
}); |
236 |
return members.iterator().hasNext(); |
237 |
} |
238 |
|
239 |
/** |
240 |
* @param info |
241 |
* @param simpleName of static method. |
242 |
* @return true if a static import exists with the same simple name. |
243 |
* Caveat, expect false positives on protected and default visibility methods from wildcard static imports. |
244 |
*/ |
245 |
public static boolean hasStaticImportSimpleNameClash(CompilationInfo info, String simpleName) { |
246 |
for (ImportTree i : info.getCompilationUnit().getImports()) { |
247 |
if (!i.isStatic()) { |
248 |
continue; |
249 |
} |
250 |
String q = i.getQualifiedIdentifier().toString(); |
251 |
if (q.endsWith(".*")) { //NOI18N |
252 |
TypeElement ie = info.getElements().getTypeElement(q.substring(0, q.length() - 2)); |
253 |
if (ie == null) { |
254 |
continue; |
255 |
} |
256 |
for (Element enclosed : ie.getEnclosedElements()) { |
257 |
Set<Modifier> modifiers = enclosed.getModifiers(); |
258 |
if (enclosed.getKind() != ElementKind.METHOD || !modifiers.contains(Modifier.STATIC) || modifiers.contains(Modifier.PRIVATE)) { |
259 |
continue; |
260 |
} |
261 |
String sn1 = enclosed.getSimpleName().toString(); |
262 |
if (simpleName.equals(sn1)) { |
263 |
return true; |
264 |
} |
265 |
} |
266 |
} else { |
267 |
int endIndex = q.lastIndexOf("."); //NOI18N |
268 |
if (endIndex == -1 || endIndex >= q.length() - 1) { |
269 |
continue; |
270 |
} |
271 |
if (q.substring(endIndex).equals(simpleName)) { |
272 |
return true; |
273 |
} |
274 |
} |
275 |
} |
276 |
return false; |
277 |
} |
278 |
|
279 |
/** |
280 |
* @param info |
281 |
* @param t1 |
282 |
* @param t3 |
283 |
* @return true iff the first type (or its containing class in the case of inner classes) |
284 |
* is a subtype of the second. |
285 |
* @see Types#isSubtype(javax.lang.model.type.TypeMirror, javax.lang.model.type.TypeMirror) |
286 |
*/ |
287 |
private static boolean isSubTypeOrInnerOfSubType(CompilationInfo info, Element t1, Element t2) { |
288 |
boolean isSubtype = info.getTypes().isSubtype(t1.asType(), t2.asType()); |
289 |
boolean isInnerClass = t1.getEnclosingElement().getKind() == ElementKind.CLASS; |
290 |
return isSubtype || (isInnerClass && info.getTypes().isSubtype(t1.getEnclosingElement().asType(), t2.asType())); |
291 |
} |
292 |
|
293 |
/** |
294 |
* @param info |
295 |
* @param klass the element for a CLASS |
296 |
* @param member the STATIC, MEMBER_SELECT Element for a MethodInvocationTree |
297 |
* @return true if member has a simple name which would clash with local or inherited |
298 |
* methods in klass (which may be an inner or static class). |
299 |
*/ |
300 |
public static boolean hasMethodNameClash(CompilationInfo info, Element klass, String simpleName) { |
301 |
assert klass != null; |
302 |
assert klass.getKind() == ElementKind.CLASS; |
303 |
|
304 |
// check the members and inherited members of the klass |
305 |
if (hasMethodWithSimpleName(info, klass, simpleName)) { |
306 |
return true; |
307 |
} |
308 |
Element klassEnclosing = klass.getEnclosingElement(); |
309 |
return (klassEnclosing != null && klassEnclosing.getKind() == ElementKind.CLASS && hasMethodWithSimpleName(info, klassEnclosing, simpleName)); |
310 |
} |
311 |
|
312 |
/** |
313 |
* @param e |
314 |
* @return the FQN for a METHOD Element |
315 |
*/ |
316 |
public static String getMethodFqn(Element e) { |
317 |
// XXX or alternatively, upgrade getElementName to handle METHOD |
318 |
assert e.getKind() == ElementKind.METHOD; |
319 |
return getElementName(e.getEnclosingElement(), true) + "." + e.getSimpleName(); |
320 |
} |
321 |
|
322 |
/** |
323 |
* @param tp |
324 |
* @return the first path which is a CLASS or null if none found |
325 |
*/ |
326 |
public static TreePath getContainingClass(TreePath tp) { |
327 |
while (tp != null && tp.getLeaf().getKind() != Kind.CLASS) { |
328 |
tp = tp.getParentPath(); |
329 |
} |
330 |
return tp; |
331 |
} |
332 |
|
333 |
// return true if the fqn already has a static import |
334 |
private static boolean isStaticallyImported(CompilationInfo info, String fqn) { |
335 |
for (ImportTree i : info.getCompilationUnit().getImports()) { |
336 |
if (!i.isStatic()) { |
337 |
continue; |
338 |
} |
339 |
String q = i.getQualifiedIdentifier().toString(); |
340 |
if (q.endsWith(".*") && fqn.startsWith(q.substring(0, q.length() - 1))) { //NOI18N |
341 |
return true; |
342 |
} |
343 |
if (q.equals(fqn)) { |
344 |
return true; |
345 |
} |
346 |
} |
347 |
return false; |
348 |
} |
349 |
|
350 |
/** |
351 |
* @param info |
352 |
* @param fqn of the containing class |
353 |
* @param simpleName of the method |
354 |
* @return true if {@code fqn.simpleName} represents a valid static method |
355 |
*/ |
356 |
public static boolean isValidStaticMethod(CompilationInfo info, String fqn, String simpleName) { |
357 |
TypeElement ie = info.getElements().getTypeElement(fqn); |
358 |
if (ie == null) { |
359 |
return false; |
360 |
} |
361 |
for (Element enclosed : ie.getEnclosedElements()) { |
362 |
Set<Modifier> modifiers = enclosed.getModifiers(); |
363 |
if (enclosed.getKind() != ElementKind.METHOD || !modifiers.contains(Modifier.STATIC) || modifiers.contains(Modifier.PRIVATE)) { |
364 |
continue; |
365 |
} |
366 |
String sn1 = enclosed.getSimpleName().toString(); |
367 |
if (simpleName.equals(sn1)) { |
368 |
return true; |
369 |
} |
370 |
} |
371 |
return false; |
372 |
} |
373 |
} |