aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/lint/AddSuppressAnnotation.java
diff options
context:
space:
mode:
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.java424
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));
+ }
+ }
+ }
+ }
+}