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