diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java | 424 |
1 files changed, 424 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java new file mode 100644 index 000000000..1a7fe5697 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java @@ -0,0 +1,424 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Eclipse Public License, Version 1.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.eclipse.org/org/documents/epl-v10.php + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.ide.eclipse.adt.internal.lint; + +import static com.android.SdkConstants.FQCN_SUPPRESS_LINT; +import static com.android.SdkConstants.FQCN_TARGET_API; +import static com.android.SdkConstants.SUPPRESS_LINT; +import static com.android.SdkConstants.TARGET_API; +import static org.eclipse.jdt.core.dom.ArrayInitializer.EXPRESSIONS_PROPERTY; +import static org.eclipse.jdt.core.dom.SingleMemberAnnotation.VALUE_PROPERTY; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.sdklib.SdkVersionInfo; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.AdtUtils; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.tools.lint.checks.AnnotationDetector; +import com.android.tools.lint.checks.ApiDetector; +import com.android.tools.lint.detector.api.Issue; +import com.android.tools.lint.detector.api.Scope; + +import org.eclipse.core.resources.IMarker; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.NullProgressMonitor; +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.dom.AST; +import org.eclipse.jdt.core.dom.ASTNode; +import org.eclipse.jdt.core.dom.AnonymousClassDeclaration; +import org.eclipse.jdt.core.dom.ArrayInitializer; +import org.eclipse.jdt.core.dom.BodyDeclaration; +import org.eclipse.jdt.core.dom.CompilationUnit; +import org.eclipse.jdt.core.dom.Expression; +import org.eclipse.jdt.core.dom.FieldDeclaration; +import org.eclipse.jdt.core.dom.MethodDeclaration; +import org.eclipse.jdt.core.dom.NodeFinder; +import org.eclipse.jdt.core.dom.SingleMemberAnnotation; +import org.eclipse.jdt.core.dom.StringLiteral; +import org.eclipse.jdt.core.dom.TypeDeclaration; +import org.eclipse.jdt.core.dom.VariableDeclarationFragment; +import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; +import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; +import org.eclipse.jdt.core.dom.rewrite.ListRewrite; +import org.eclipse.jdt.ui.IWorkingCopyManager; +import org.eclipse.jdt.ui.JavaUI; +import org.eclipse.jdt.ui.SharedASTProvider; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.graphics.Image; +import org.eclipse.text.edits.MultiTextEdit; +import org.eclipse.text.edits.TextEdit; +import org.eclipse.ui.IEditorInput; +import org.eclipse.ui.IMarkerResolution; +import org.eclipse.ui.IMarkerResolution2; +import org.eclipse.ui.texteditor.IDocumentProvider; +import org.eclipse.ui.texteditor.ITextEditor; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Marker resolution for adding {@code @SuppressLint} annotations in Java files. + * It can also add {@code @TargetApi} annotations. + */ +class AddSuppressAnnotation implements IMarkerResolution2 { + private final IMarker mMarker; + private final String mId; + private final BodyDeclaration mNode; + private final String mDescription; + /** + * Should it create a {@code @TargetApi} annotation instead of + * {@code SuppressLint} ? If so pass a non null API level + */ + private final String mTargetApi; + + private AddSuppressAnnotation( + @NonNull String id, + @NonNull IMarker marker, + @NonNull BodyDeclaration node, + @NonNull String description, + @Nullable String targetApi) { + mId = id; + mMarker = marker; + mNode = node; + mDescription = description; + mTargetApi = targetApi; + } + + @Override + public String getLabel() { + return mDescription; + } + + @Override + public String getDescription() { + return null; + } + + @Override + public Image getImage() { + return IconFactory.getInstance().getIcon("newannotation"); //$NON-NLS-1$ + } + + @Override + public void run(IMarker marker) { + ITextEditor textEditor = AdtUtils.getActiveTextEditor(); + IDocumentProvider provider = textEditor.getDocumentProvider(); + IEditorInput editorInput = textEditor.getEditorInput(); + IDocument document = provider.getDocument(editorInput); + if (document == null) { + return; + } + IWorkingCopyManager manager = JavaUI.getWorkingCopyManager(); + ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput); + try { + MultiTextEdit edit; + if (mTargetApi == null) { + edit = addSuppressAnnotation(document, compilationUnit, mNode); + } else { + edit = addTargetApiAnnotation(document, compilationUnit, mNode); + } + if (edit != null) { + edit.apply(document); + + // Remove the marker now that the suppress annotation has been added + // (so the user doesn't have to re-run lint just to see it disappear, + // and besides we don't want to keep offering marker resolutions on this + // marker which could lead to duplicate annotations since the above code + // assumes that the current id isn't in the list of values, since otherwise + // lint shouldn't have complained here. + mMarker.delete(); + } + } catch (Exception ex) { + AdtPlugin.log(ex, "Could not add suppress annotation"); + } + } + + @SuppressWarnings({"rawtypes"}) // Java AST API has raw types + private MultiTextEdit addSuppressAnnotation( + IDocument document, + ICompilationUnit compilationUnit, + BodyDeclaration declaration) throws CoreException { + List modifiers = declaration.modifiers(); + SingleMemberAnnotation existing = null; + for (Object o : modifiers) { + if (o instanceof SingleMemberAnnotation) { + SingleMemberAnnotation annotation = (SingleMemberAnnotation) o; + String type = annotation.getTypeName().getFullyQualifiedName(); + if (type.equals(FQCN_SUPPRESS_LINT) || type.endsWith(SUPPRESS_LINT)) { + existing = annotation; + break; + } + } + } + + ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true); + String local = importRewrite.addImport(FQCN_SUPPRESS_LINT); + AST ast = declaration.getAST(); + ASTRewrite rewriter = ASTRewrite.create(ast); + if (existing == null) { + SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation(); + newAnnotation.setTypeName(ast.newSimpleName(local)); + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + newAnnotation.setValue(value); + ListRewrite listRewrite = rewriter.getListRewrite(declaration, + declaration.getModifiersProperty()); + listRewrite.insertFirst(newAnnotation, null); + } else { + Expression existingValue = existing.getValue(); + if (existingValue instanceof StringLiteral) { + StringLiteral stringLiteral = (StringLiteral) existingValue; + if (mId.equals(stringLiteral.getLiteralValue())) { + // Already contains the id + return null; + } + // Create a new array initializer holding the old string plus the new id + ArrayInitializer array = ast.newArrayInitializer(); + StringLiteral old = ast.newStringLiteral(); + old.setLiteralValue(stringLiteral.getLiteralValue()); + array.expressions().add(old); + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + array.expressions().add(value); + rewriter.set(existing, VALUE_PROPERTY, array, null); + } else if (existingValue instanceof ArrayInitializer) { + // Existing array: just append the new string + ArrayInitializer array = (ArrayInitializer) existingValue; + List expressions = array.expressions(); + if (expressions != null) { + for (Object o : expressions) { + if (o instanceof StringLiteral) { + if (mId.equals(((StringLiteral)o).getLiteralValue())) { + // Already contains the id + return null; + } + } + } + } + StringLiteral value = ast.newStringLiteral(); + value.setLiteralValue(mId); + ListRewrite listRewrite = rewriter.getListRewrite(array, EXPRESSIONS_PROPERTY); + listRewrite.insertLast(value, null); + } else { + assert false : existingValue; + return null; + } + } + + TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor()); + TextEdit annotationEdits = rewriter.rewriteAST(document, null); + + // Apply to the document + MultiTextEdit edit = new MultiTextEdit(); + // Create the edit to change the imports, only if + // anything changed + if (importEdits.hasChildren()) { + edit.addChild(importEdits); + } + edit.addChild(annotationEdits); + + return edit; + } + + @SuppressWarnings({"rawtypes"}) // Java AST API has raw types + private MultiTextEdit addTargetApiAnnotation( + IDocument document, + ICompilationUnit compilationUnit, + BodyDeclaration declaration) throws CoreException { + List modifiers = declaration.modifiers(); + SingleMemberAnnotation existing = null; + for (Object o : modifiers) { + if (o instanceof SingleMemberAnnotation) { + SingleMemberAnnotation annotation = (SingleMemberAnnotation) o; + String type = annotation.getTypeName().getFullyQualifiedName(); + if (type.equals(FQCN_TARGET_API) || type.endsWith(TARGET_API)) { + existing = annotation; + break; + } + } + } + + ImportRewrite importRewrite = ImportRewrite.create(compilationUnit, true); + importRewrite.addImport("android.os.Build"); //$NON-NLS-1$ + String local = importRewrite.addImport(FQCN_TARGET_API); + AST ast = declaration.getAST(); + ASTRewrite rewriter = ASTRewrite.create(ast); + if (existing == null) { + SingleMemberAnnotation newAnnotation = ast.newSingleMemberAnnotation(); + newAnnotation.setTypeName(ast.newSimpleName(local)); + Expression value = createLiteral(ast); + newAnnotation.setValue(value); + ListRewrite listRewrite = rewriter.getListRewrite(declaration, + declaration.getModifiersProperty()); + listRewrite.insertFirst(newAnnotation, null); + } else { + Expression value = createLiteral(ast); + rewriter.set(existing, VALUE_PROPERTY, value, null); + } + + TextEdit importEdits = importRewrite.rewriteImports(new NullProgressMonitor()); + TextEdit annotationEdits = rewriter.rewriteAST(document, null); + MultiTextEdit edit = new MultiTextEdit(); + if (importEdits.hasChildren()) { + edit.addChild(importEdits); + } + edit.addChild(annotationEdits); + + return edit; + } + + private Expression createLiteral(AST ast) { + Expression value; + if (!isCodeName()) { + value = ast.newQualifiedName( + ast.newQualifiedName(ast.newSimpleName("Build"), //$NON-NLS-1$ + ast.newSimpleName("VERSION_CODES")), //$NON-NLS-1$ + ast.newSimpleName(mTargetApi)); + } else { + value = ast.newNumberLiteral(mTargetApi); + } + return value; + } + + private boolean isCodeName() { + return Character.isDigit(mTargetApi.charAt(0)); + } + + /** + * Adds any applicable suppress lint fix resolutions into the given list + * + * @param marker the marker to create fixes for + * @param id the issue id + * @param resolutions a list to add the created resolutions into, if any + */ + public static void createFixes(IMarker marker, String id, + List<IMarkerResolution> resolutions) { + ITextEditor textEditor = AdtUtils.getActiveTextEditor(); + IDocumentProvider provider = textEditor.getDocumentProvider(); + IEditorInput editorInput = textEditor.getEditorInput(); + IDocument document = provider.getDocument(editorInput); + if (document == null) { + return; + } + + IWorkingCopyManager manager = JavaUI.getWorkingCopyManager(); + ICompilationUnit compilationUnit = manager.getWorkingCopy(editorInput); + int offset = 0; + int length = 0; + int start = marker.getAttribute(IMarker.CHAR_START, -1); + int end = marker.getAttribute(IMarker.CHAR_END, -1); + offset = start; + length = end - start; + CompilationUnit root = SharedASTProvider.getAST(compilationUnit, + SharedASTProvider.WAIT_YES, null); + if (root == null) { + return; + } + + int api = -1; + if (id.equals(ApiDetector.UNSUPPORTED.getId()) || + id.equals(ApiDetector.INLINED.getId())) { + String message = marker.getAttribute(IMarker.MESSAGE, null); + if (message != null) { + Pattern pattern = Pattern.compile("\\s(\\d+)\\s"); //$NON-NLS-1$ + Matcher matcher = pattern.matcher(message); + if (matcher.find()) { + api = Integer.parseInt(matcher.group(1)); + } + } + } + + Issue issue = EclipseLintClient.getRegistry().getIssue(id); + boolean isClassDetector = issue != null && issue.getImplementation().getScope().contains( + Scope.CLASS_FILE); + + // Don't offer to suppress (with an annotation) the annotation checks + if (issue == AnnotationDetector.ISSUE) { + return; + } + + NodeFinder nodeFinder = new NodeFinder(root, offset, length); + ASTNode coveringNode; + if (offset <= 0) { + // Error added on the first line of a Java class: typically from a class-based + // detector which lacks line information. Map this to the top level class + // in the file instead. + coveringNode = root; + if (root.types() != null && root.types().size() > 0) { + Object type = root.types().get(0); + if (type instanceof ASTNode) { + coveringNode = (ASTNode) type; + } + } + } else { + coveringNode = nodeFinder.getCoveringNode(); + } + for (ASTNode body = coveringNode; body != null; body = body.getParent()) { + if (body instanceof BodyDeclaration) { + BodyDeclaration declaration = (BodyDeclaration) body; + + String target = null; + if (body instanceof MethodDeclaration) { + target = ((MethodDeclaration) body).getName().toString() + "()"; //$NON-NLS-1$ + } else if (body instanceof FieldDeclaration) { + target = "field"; + FieldDeclaration field = (FieldDeclaration) body; + if (field.fragments() != null && field.fragments().size() > 0) { + ASTNode first = (ASTNode) field.fragments().get(0); + if (first instanceof VariableDeclarationFragment) { + VariableDeclarationFragment decl = (VariableDeclarationFragment) first; + target = decl.getName().toString(); + } + } + } else if (body instanceof AnonymousClassDeclaration) { + target = "anonymous class"; + } else if (body instanceof TypeDeclaration) { + target = ((TypeDeclaration) body).getName().toString(); + } else { + target = body.getClass().getSimpleName(); + } + + // In class files, detectors can only find annotations on methods + // and on classes, not on variable declarations + if (isClassDetector && !(body instanceof MethodDeclaration + || body instanceof TypeDeclaration + || body instanceof AnonymousClassDeclaration + || body instanceof FieldDeclaration)) { + continue; + } + + String desc = String.format("Add @SuppressLint '%1$s\' to '%2$s'", id, target); + resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, null)); + + if (api != -1 + // @TargetApi is only valid on methods and classes, not fields etc + && (body instanceof MethodDeclaration + || body instanceof TypeDeclaration)) { + String apiString = SdkVersionInfo.getBuildCode(api); + if (apiString == null) { + apiString = Integer.toString(api); + } + desc = String.format("Add @TargetApi(%1$s) to '%2$s'", apiString, target); + resolutions.add(new AddSuppressAnnotation(id, marker, declaration, desc, + apiString)); + } + } + } + } +} |