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