Home | History | Annotate | Download | only in lint
      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.ide.common.sdk.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 
     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
    104     public String getLabel() {
    105         return mDescription;
    106     }
    107 
    108     @Override
    109     public String getDescription() {
    110         return null;
    111     }
    112 
    113     @Override
    114     public Image getImage() {
    115         return IconFactory.getInstance().getIcon("newannotation"); //$NON-NLS-1$
    116     }
    117 
    118     @Override
    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
    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
    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 
    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 
    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      */
    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