• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2012 The Android Open Source Project
3  *
4  * Licensed under the Eclipse Public License, Version 1.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.eclipse.org/org/documents/epl-v10.php
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.ide.eclipse.adt.internal.lint;
18 
19 import static com.android.tools.lint.detector.api.LintConstants.FQCN_SUPPRESS_LINT;
20 import static com.android.tools.lint.detector.api.LintConstants.FQCN_TARGET_API;
21 import static com.android.tools.lint.detector.api.LintConstants.SUPPRESS_LINT;
22 import static com.android.tools.lint.detector.api.LintConstants.TARGET_API;
23 import static org.eclipse.jdt.core.dom.ArrayInitializer.EXPRESSIONS_PROPERTY;
24 import static org.eclipse.jdt.core.dom.SingleMemberAnnotation.VALUE_PROPERTY;
25 
26 import com.android.ide.eclipse.adt.AdtPlugin;
27 import com.android.ide.eclipse.adt.AdtUtils;
28 import com.android.ide.eclipse.adt.internal.editors.IconFactory;
29 import com.android.tools.lint.checks.ApiDetector;
30 import com.android.tools.lint.detector.api.Issue;
31 import com.android.tools.lint.detector.api.Scope;
32 
33 import org.eclipse.core.resources.IMarker;
34 import org.eclipse.core.runtime.CoreException;
35 import org.eclipse.core.runtime.NullProgressMonitor;
36 import org.eclipse.jdt.core.ICompilationUnit;
37 import org.eclipse.jdt.core.dom.AST;
38 import org.eclipse.jdt.core.dom.ASTNode;
39 import org.eclipse.jdt.core.dom.AnonymousClassDeclaration;
40 import org.eclipse.jdt.core.dom.ArrayInitializer;
41 import org.eclipse.jdt.core.dom.BodyDeclaration;
42 import org.eclipse.jdt.core.dom.CompilationUnit;
43 import org.eclipse.jdt.core.dom.Expression;
44 import org.eclipse.jdt.core.dom.FieldDeclaration;
45 import org.eclipse.jdt.core.dom.MethodDeclaration;
46 import org.eclipse.jdt.core.dom.NodeFinder;
47 import org.eclipse.jdt.core.dom.NumberLiteral;
48 import org.eclipse.jdt.core.dom.SingleMemberAnnotation;
49 import org.eclipse.jdt.core.dom.StringLiteral;
50 import org.eclipse.jdt.core.dom.TypeDeclaration;
51 import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
52 import org.eclipse.jdt.core.dom.rewrite.ASTRewrite;
53 import org.eclipse.jdt.core.dom.rewrite.ImportRewrite;
54 import org.eclipse.jdt.core.dom.rewrite.ListRewrite;
55 import org.eclipse.jdt.ui.IWorkingCopyManager;
56 import org.eclipse.jdt.ui.JavaUI;
57 import org.eclipse.jdt.ui.SharedASTProvider;
58 import org.eclipse.jface.text.IDocument;
59 import org.eclipse.swt.graphics.Image;
60 import org.eclipse.text.edits.MultiTextEdit;
61 import org.eclipse.text.edits.TextEdit;
62 import org.eclipse.ui.IEditorInput;
63 import org.eclipse.ui.IMarkerResolution;
64 import org.eclipse.ui.IMarkerResolution2;
65 import org.eclipse.ui.texteditor.IDocumentProvider;
66 import org.eclipse.ui.texteditor.ITextEditor;
67 
68 import java.util.List;
69 import java.util.regex.Matcher;
70 import java.util.regex.Pattern;
71 
72 /**
73  * Marker resolution for adding {@code @SuppressLint} annotations in Java files.
74  * It can also add {@code @TargetApi} annotations.
75  */
76 class AddSuppressAnnotation implements IMarkerResolution2 {
77     private final IMarker mMarker;
78     private final String mId;
79     private final BodyDeclaration mNode;
80     private final String mDescription;
81     /** Should it create a {@code @TargetApi} annotation instead of {@code SuppressLint} ?
82      * If so pass a positive API number */
83     private final int mTargetApi;
84 
AddSuppressAnnotation(String id, IMarker marker, BodyDeclaration node, String description, int targetApi)85     private AddSuppressAnnotation(String id, IMarker marker, BodyDeclaration node,
86             String description, int targetApi) {
87         mId = id;
88         mMarker = marker;
89         mNode = node;
90         mDescription = description;
91         mTargetApi = targetApi;
92     }
93 
94     @Override
getLabel()95     public String getLabel() {
96         return mDescription;
97     }
98 
99     @Override
getDescription()100     public String getDescription() {
101         return null;
102     }
103 
104     @Override
getImage()105     public Image getImage() {
106         return IconFactory.getInstance().getIcon("newannotation"); //$NON-NLS-1$
107     }
108 
109     @Override
run(IMarker marker)110     public void run(IMarker marker) {
111         ITextEditor textEditor = AdtUtils.getActiveTextEditor();
112         IDocumentProvider provider = textEditor.getDocumentProvider();
113         IEditorInput editorInput = textEditor.getEditorInput();
114         IDocument document = provider.getDocument(editorInput);
115         if (document == null) {
116             return;
117         }
118         IWorkingCopyManager manager = JavaUI.getWorkingCopyManager();
119         ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput);
120         try {
121             MultiTextEdit edit;
122             if (mTargetApi <= 0) {
123                 edit = addSuppressAnnotation(document, compilationUnit, mNode);
124             } else {
125                 edit = addTargetApiAnnotation(document, compilationUnit, mNode);
126             }
127             if (edit != null) {
128                 edit.apply(document);
129 
130                 // Remove the marker now that the suppress annotation has been added
131                 // (so the user doesn't have to re-run lint just to see it disappear,
132                 // and besides we don't want to keep offering marker resolutions on this
133                 // marker which could lead to duplicate annotations since the above code
134                 // assumes that the current id isn't in the list of values, since otherwise
135                 // lint shouldn't have complained here.
136                 mMarker.delete();
137             }
138         } catch (Exception ex) {
139             AdtPlugin.log(ex, "Could not add suppress annotation");
140         }
141     }
142 
143     @SuppressWarnings({"rawtypes"}) // Java AST API has raw types
addSuppressAnnotation( IDocument document, ICompilationUnit compilationUnit, BodyDeclaration declaration)144     private MultiTextEdit addSuppressAnnotation(
145             IDocument document,
146             ICompilationUnit compilationUnit,
147             BodyDeclaration declaration) throws CoreException {
148         List modifiers = declaration.modifiers();
149         SingleMemberAnnotation existing = null;
150         for (Object o : modifiers) {
151             if (o instanceof SingleMemberAnnotation) {
152                 SingleMemberAnnotation annotation = (SingleMemberAnnotation) o;
153                 String type = annotation.getTypeName().getFullyQualifiedName();
154                 if (type.equals(FQCN_SUPPRESS_LINT) || type.endsWith(SUPPRESS_LINT)) {
155                     existing = annotation;
156                     break;
157                 }
158             }
159         }
160 
161         ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true);
162         String local = importRewrite.addImport(FQCN_SUPPRESS_LINT);
163         AST ast = declaration.getAST();
164         ASTRewrite rewriter = ASTRewrite.create(ast);
165         if (existing == null) {
166             SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation();
167             newAnnotation.setTypeName(ast.newSimpleName(local));
168             StringLiteral value = ast.newStringLiteral();
169             value.setLiteralValue(mId);
170             newAnnotation.setValue(value);
171             ListRewrite listRewrite = rewriter.getListRewrite(declaration,
172                     declaration.getModifiersProperty());
173             listRewrite.insertFirst(newAnnotation, null);
174         } else {
175             Expression existingValue = existing.getValue();
176             if (existingValue instanceof StringLiteral) {
177                 // Create a new array initializer holding the old string plus the new id
178                 ArrayInitializer array = ast.newArrayInitializer();
179                 StringLiteral old = ast.newStringLiteral();
180                 StringLiteral stringLiteral = (StringLiteral) existingValue;
181                 old.setLiteralValue(stringLiteral.getLiteralValue());
182                 array.expressions().add(old);
183                 StringLiteral value = ast.newStringLiteral();
184                 value.setLiteralValue(mId);
185                 array.expressions().add(value);
186                 rewriter.set(existing, VALUE_PROPERTY, array, null);
187             } else if (existingValue instanceof ArrayInitializer) {
188                 // Existing array: just append the new string
189                 ArrayInitializer array = (ArrayInitializer) existingValue;
190                 StringLiteral value = ast.newStringLiteral();
191                 value.setLiteralValue(mId);
192                 ListRewrite listRewrite = rewriter.getListRewrite(array, EXPRESSIONS_PROPERTY);
193                 listRewrite.insertLast(value, null);
194             } else {
195                 assert false : existingValue;
196                 return null;
197             }
198         }
199 
200         TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor());
201         TextEdit annotationEdits = rewriter.rewriteAST(document, null);
202 
203         // Apply to the document
204         MultiTextEdit edit = new MultiTextEdit();
205         // Create the edit to change the imports, only if
206         // anything changed
207         if (importEdits.hasChildren()) {
208             edit.addChild(importEdits);
209         }
210         edit.addChild(annotationEdits);
211 
212         return edit;
213     }
214 
215     @SuppressWarnings({"rawtypes"}) // Java AST API has raw types
addTargetApiAnnotation( IDocument document, ICompilationUnit compilationUnit, BodyDeclaration declaration)216     private MultiTextEdit addTargetApiAnnotation(
217             IDocument document,
218             ICompilationUnit compilationUnit,
219             BodyDeclaration declaration) throws CoreException {
220         List modifiers = declaration.modifiers();
221         SingleMemberAnnotation existing = null;
222         for (Object o : modifiers) {
223             if (o instanceof SingleMemberAnnotation) {
224                 SingleMemberAnnotation annotation = (SingleMemberAnnotation) o;
225                 String type = annotation.getTypeName().getFullyQualifiedName();
226                 if (type.equals(FQCN_TARGET_API) || type.endsWith(TARGET_API)) {
227                     existing = annotation;
228                     break;
229                 }
230             }
231         }
232 
233         ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true);
234         String local = importRewrite.addImport(FQCN_TARGET_API);
235         AST ast = declaration.getAST();
236         ASTRewrite rewriter = ASTRewrite.create(ast);
237         if (existing == null) {
238             SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation();
239             newAnnotation.setTypeName(ast.newSimpleName(local));
240             NumberLiteral value = ast.newNumberLiteral(Integer.toString(mTargetApi));
241             //value.setLiteralValue(mId);
242             newAnnotation.setValue(value);
243             ListRewrite listRewrite = rewriter.getListRewrite(declaration,
244                     declaration.getModifiersProperty());
245             listRewrite.insertFirst(newAnnotation, null);
246         } else {
247             Expression existingValue = existing.getValue();
248             if (existingValue instanceof NumberLiteral) {
249                 // Change the value to the new value
250                 NumberLiteral value = ast.newNumberLiteral(Integer.toString(mTargetApi));
251                 rewriter.set(existing, VALUE_PROPERTY, value, null);
252             } else {
253                 assert false : existingValue;
254                 return null;
255             }
256         }
257 
258         TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor());
259         TextEdit annotationEdits = rewriter.rewriteAST(document, null);
260         MultiTextEdit edit = new MultiTextEdit();
261         if (importEdits.hasChildren()) {
262             edit.addChild(importEdits);
263         }
264         edit.addChild(annotationEdits);
265 
266         return edit;
267     }
268 
269     /**
270      * Adds any applicable suppress lint fix resolutions into the given list
271      *
272      * @param marker the marker to create fixes for
273      * @param id the issue id
274      * @param resolutions a list to add the created resolutions into, if any
275      */
createFixes(IMarker marker, String id, List<IMarkerResolution> resolutions)276     public static void createFixes(IMarker marker, String id,
277             List<IMarkerResolution> resolutions) {
278         ITextEditor textEditor = AdtUtils.getActiveTextEditor();
279         IDocumentProvider provider = textEditor.getDocumentProvider();
280         IEditorInput editorInput = textEditor.getEditorInput();
281         IDocument document = provider.getDocument(editorInput);
282         if (document == null) {
283             return;
284         }
285         IWorkingCopyManager manager = JavaUI.getWorkingCopyManager();
286         ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput);
287         int offset = 0;
288         int length = 0;
289         int start = marker.getAttribute(IMarker.CHAR_START, -1);
290         int end = marker.getAttribute(IMarker.CHAR_END, -1);
291         offset = start;
292         length = end - start;
293         CompilationUnit root = SharedASTProvider.getAST(compilationUnit,
294                 SharedASTProvider.WAIT_YES, null);
295         if (root == null) {
296             return;
297         }
298 
299         int api = -1;
300         if (id.equals(ApiDetector.UNSUPPORTED.getId())) {
301             String message = marker.getAttribute(IMarker.MESSAGE, null);
302             if (message != null) {
303                 Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$
304                 Matcher matcher = pattern.matcher(message);
305                 if (matcher.find()) {
306                     api = Integer.parseInt(matcher.group(1));
307                 }
308             }
309         }
310 
311         Issue issue = EclipseLintClient.getRegistry().getIssue(id);
312         boolean isClassDetector = issue != null && issue.getScope().contains(Scope.CLASS_FILE);
313 
314         NodeFinder nodeFinder = new NodeFinder(root, offset, length);
315         ASTNode coveringNode;
316         if (offset <= 0) {
317             // Error added on the first line of a Java class: typically from a class-based
318             // detector which lacks line information. Map this to the top level class
319             // in the file instead.
320             coveringNode = root;
321             if (root.types() != null && root.types().size() > 0) {
322                 Object type = root.types().get(0);
323                 if (type instanceof ASTNode) {
324                     coveringNode = (ASTNode) type;
325                 }
326             }
327         } else {
328             coveringNode = nodeFinder.getCoveringNode();
329         }
330         for (ASTNode body = coveringNode; body != null; body = body.getParent()) {
331             if (body instanceof BodyDeclaration) {
332                 BodyDeclaration declaration = (BodyDeclaration) body;
333 
334                 String target = null;
335                 if (body instanceof MethodDeclaration) {
336                     target = ((MethodDeclaration) body).getName().toString() + "()"; //$NON-NLS-1$
337                 } else if (body instanceof FieldDeclaration) {
338                     target = "field";
339                     FieldDeclaration field = (FieldDeclaration) body;
340                     if (field.fragments() != null && field.fragments().size() > 0) {
341                         ASTNode first = (ASTNode) field.fragments().get(0);
342                         if (first instanceof VariableDeclarationFragment) {
343                             VariableDeclarationFragment decl = (VariableDeclarationFragment) first;
344                             target = decl.getName().toString();
345                         }
346                     }
347                 } else if (body instanceof AnonymousClassDeclaration) {
348                     target = "anonymous class";
349                 } else if (body instanceof TypeDeclaration) {
350                     target = ((TypeDeclaration) body).getName().toString();
351                 } else {
352                     target = body.getClass().getSimpleName();
353                 }
354 
355                 // In class files, detectors can only find annotations on methods
356                 // and on classes, not on variable declarations
357                 if (isClassDetector && !(body instanceof MethodDeclaration
358                             || body instanceof TypeDeclaration
359                             || body instanceof AnonymousClassDeclaration
360                             || body instanceof FieldDeclaration)) {
361                     continue;
362                 }
363 
364                 String desc = String.format("Add @SuppressLint '%1$s\' to '%2$s'", id, target);
365                 resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, -1));
366 
367                 if (api != -1
368                         // @TargetApi is only valid on methods and classes, not fields etc
369                         && (body instanceof MethodDeclaration
370                                 || body instanceof TypeDeclaration)) {
371                     desc = String.format("Add @TargetApi(%1$d) to '%2$s'", api, target);
372                     resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, api));
373                 }
374             }
375         }
376     }
377 }
378