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