/* * 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.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())) { 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.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 = AdtUtils.getBuildCodes(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)); } } } } }