Home | History | Annotate | Download | only in lint
      1 /*
      2  * Copyright (C) 2011 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 package com.android.ide.eclipse.adt.internal.lint;
     17 
     18 import static com.android.ide.common.layout.LayoutConstants.ANDROID_URI;
     19 import static com.android.ide.common.layout.LayoutConstants.ATTR_CONTENT_DESCRIPTION;
     20 import static com.android.ide.common.layout.LayoutConstants.ATTR_INPUT_TYPE;
     21 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_HEIGHT;
     22 import static com.android.ide.common.layout.LayoutConstants.ATTR_LAYOUT_WIDTH;
     23 import static com.android.ide.common.layout.LayoutConstants.ATTR_ORIENTATION;
     24 import static com.android.ide.common.layout.LayoutConstants.VALUE_N_DP;
     25 import static com.android.ide.common.layout.LayoutConstants.VALUE_VERTICAL;
     26 import static com.android.ide.common.layout.LayoutConstants.VALUE_ZERO_DP;
     27 
     28 import com.android.ide.eclipse.adt.AdtPlugin;
     29 import com.android.ide.eclipse.adt.AdtUtils;
     30 import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
     31 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
     32 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.DomUtilities;
     33 import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.UnwrapRefactoring;
     34 import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;
     35 import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringRefactoring;
     36 import com.android.ide.eclipse.adt.internal.refactorings.extractstring.ExtractStringWizard;
     37 import com.android.tools.lint.checks.AccessibilityDetector;
     38 import com.android.tools.lint.checks.BuiltinDetectorRegistry;
     39 import com.android.tools.lint.checks.HardcodedValuesDetector;
     40 import com.android.tools.lint.checks.InefficientWeightDetector;
     41 import com.android.tools.lint.checks.PxUsageDetector;
     42 import com.android.tools.lint.checks.TextFieldDetector;
     43 import com.android.tools.lint.checks.UselessViewDetector;
     44 import com.android.tools.lint.detector.api.Issue;
     45 
     46 import org.eclipse.core.resources.IFile;
     47 import org.eclipse.core.resources.IMarker;
     48 import org.eclipse.core.runtime.CoreException;
     49 import org.eclipse.jface.dialogs.IInputValidator;
     50 import org.eclipse.jface.dialogs.InputDialog;
     51 import org.eclipse.jface.text.IDocument;
     52 import org.eclipse.jface.text.ITextSelection;
     53 import org.eclipse.jface.text.Region;
     54 import org.eclipse.jface.text.TextSelection;
     55 import org.eclipse.jface.text.contentassist.ICompletionProposal;
     56 import org.eclipse.jface.text.contentassist.IContextInformation;
     57 import org.eclipse.jface.window.Window;
     58 import org.eclipse.ltk.ui.refactoring.RefactoringWizard;
     59 import org.eclipse.ltk.ui.refactoring.RefactoringWizardOpenOperation;
     60 import org.eclipse.swt.graphics.Image;
     61 import org.eclipse.swt.graphics.Point;
     62 import org.eclipse.ui.IEditorPart;
     63 import org.eclipse.ui.ISharedImages;
     64 import org.eclipse.ui.IWorkbenchWindow;
     65 import org.eclipse.ui.PartInitException;
     66 import org.eclipse.ui.PlatformUI;
     67 import org.eclipse.wst.sse.core.StructuredModelManager;
     68 import org.eclipse.wst.sse.core.internal.provisional.IModelManager;
     69 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel;
     70 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion;
     71 import org.w3c.dom.Attr;
     72 import org.w3c.dom.Element;
     73 import org.w3c.dom.NamedNodeMap;
     74 import org.w3c.dom.Node;
     75 
     76 import java.lang.reflect.Constructor;
     77 import java.util.HashMap;
     78 import java.util.Map;
     79 import java.util.regex.Matcher;
     80 import java.util.regex.Pattern;
     81 
     82 @SuppressWarnings("restriction") // DOM model
     83 abstract class LintFix implements ICompletionProposal {
     84     protected final IMarker mMarker;
     85     protected final String mId;
     86 
     87     protected LintFix(String id, IMarker marker) {
     88         mId = id;
     89         mMarker = marker;
     90     }
     91 
     92     /**
     93      * Returns true if this fix needs focus (which means that when the fix is
     94      * performed from a {@link LintListDialog}'s Fix button
     95      *
     96      * @return true if this fix needs focus after being applied
     97      */
     98     public boolean needsFocus() {
     99         return true;
    100     }
    101 
    102     /**
    103      * Returns true if this fix can be performed along side other fixes
    104      *
    105      * @return true if this fix can be performed in a bulk operation with other
    106      *         fixes
    107      */
    108     public boolean isBulkCapable() {
    109         return false;
    110     }
    111 
    112     // ---- Implements ICompletionProposal ----
    113 
    114     public String getDisplayString() {
    115         return null;
    116     }
    117 
    118     public String getAdditionalProposalInfo() {
    119         Issue issue = new BuiltinDetectorRegistry().getIssue(mId);
    120         if (issue != null) {
    121             return issue.getExplanation();
    122         }
    123 
    124         return null;
    125     }
    126 
    127     public void deleteMarker() {
    128         try {
    129             mMarker.delete();
    130         } catch (PartInitException e) {
    131             AdtPlugin.log(e, null);
    132         } catch (CoreException e) {
    133             AdtPlugin.log(e, null);
    134         }
    135     }
    136 
    137     public Point getSelection(IDocument document) {
    138         return null;
    139     }
    140 
    141     public Image getImage() {
    142         ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    143         return sharedImages.getImage(ISharedImages.IMG_OBJS_WARN_TSK);
    144     }
    145 
    146     public IContextInformation getContextInformation() {
    147         return null;
    148     }
    149 
    150     // --- Access to available fixes ---
    151 
    152     private static final Map<String, Class<? extends LintFix>> sFixes =
    153             new HashMap<String, Class<? extends LintFix>>();
    154     static {
    155         sFixes.put(AccessibilityDetector.ISSUE.getId(), AccessibilityFix.class);
    156         sFixes.put(InefficientWeightDetector.ISSUE.getId(), LinearLayoutWeightFix.class);
    157         sFixes.put(HardcodedValuesDetector.ISSUE.getId(), ExtractStringFix.class);
    158         sFixes.put(UselessViewDetector.USELESS_LEAF.getId(), RemoveUselessViewFix.class);
    159         sFixes.put(UselessViewDetector.USELESS_PARENT.getId(), RemoveUselessViewFix.class);
    160         sFixes.put(PxUsageDetector.ISSUE.getId(), ConvertToDpFix.class);
    161         sFixes.put(TextFieldDetector.ISSUE.getId(), SetInputTypeFix.class);
    162     }
    163 
    164     public static boolean hasFix(String id) {
    165         return sFixes.containsKey(id);
    166     }
    167 
    168     /**
    169      * Returns a fix for the given issue, or null if no fix is available
    170      *
    171      * @param id the id o the issue to obtain a fix for (see {@link Issue#getId()})
    172      * @param marker the marker corresponding to the error
    173      * @return a fix, or null
    174      */
    175     public static LintFix getFix(String id, final IMarker marker) {
    176         Class<? extends LintFix> clazz = sFixes.get(id);
    177         if (clazz != null) {
    178             try {
    179                 Constructor<? extends LintFix> constructor = clazz.getDeclaredConstructor(
    180                         String.class, IMarker.class);
    181                 constructor.setAccessible(true);
    182                 return constructor.newInstance(id, marker);
    183             } catch (Throwable t) {
    184                 AdtPlugin.log(t, null);
    185             }
    186         }
    187 
    188         return null;
    189     }
    190 
    191     private abstract static class DocumentFix extends LintFix {
    192 
    193         protected DocumentFix(String id, IMarker marker) {
    194             super(id, marker);
    195         }
    196 
    197         protected abstract void apply(IDocument document, IStructuredModel model, Node node,
    198                 int start, int end);
    199 
    200         public void apply(IDocument document) {
    201             int start = mMarker.getAttribute(IMarker.CHAR_START, -1);
    202             int end = mMarker.getAttribute(IMarker.CHAR_END, -1);
    203             if (start != -1 && end != -1) {
    204                 Node node = DomUtilities.getNode(document, start);
    205                 IModelManager manager = StructuredModelManager.getModelManager();
    206                 IStructuredModel model = manager.getExistingModelForEdit(document);
    207                 try {
    208                     apply(document, model, node, start, end);
    209                 } finally {
    210                     model.releaseFromEdit();
    211                 }
    212 
    213                 deleteMarker();
    214             }
    215         }
    216     }
    217 
    218     private abstract static class SetPropertyFix extends DocumentFix {
    219         private Region mSelect;
    220 
    221         private SetPropertyFix(String id, IMarker marker) {
    222             super(id, marker);
    223         }
    224 
    225         /** Attribute to be added */
    226         protected abstract String getAttribute();
    227 
    228         protected String getProposal() {
    229             return "TODO";
    230         }
    231 
    232         @Override
    233         protected void apply(IDocument document, IStructuredModel model, Node node, int start,
    234                 int end) {
    235             mSelect = null;
    236 
    237             if (node instanceof Element) {
    238                 Element element = (Element) node;
    239                 String proposal = getProposal();
    240                 String localAttribute = getAttribute();
    241                 String prefix = UiElementNode.lookupNamespacePrefix(node, ANDROID_URI);
    242                 String attribute = prefix != null ? prefix + ':' + localAttribute : localAttribute;
    243 
    244                 // This does not work even though it should: it does not include the prefix
    245                 //element.setAttributeNS(ANDROID_URI, localAttribute, proposal);
    246                 // So workaround instead:
    247                 element.setAttribute(attribute, proposal);
    248 
    249                 Attr attr = element.getAttributeNodeNS(ANDROID_URI, localAttribute);
    250                 if (attr instanceof IndexedRegion) {
    251                     IndexedRegion region = (IndexedRegion) attr;
    252                     int offset = region.getStartOffset();
    253                     // We only want to select the value part inside the quotes,
    254                     // so skip the attribute and =" parts added by WST:
    255                     offset += attribute.length() + 2;
    256                     mSelect = new Region(offset, proposal.length());
    257                 }
    258             }
    259         }
    260 
    261         @Override
    262         public void apply(IDocument document) {
    263             try {
    264                 IFile file = (IFile) mMarker.getResource();
    265                 super.apply(document);
    266                 AdtPlugin.openFile(file, mSelect, true);
    267             } catch (PartInitException e) {
    268                 AdtPlugin.log(e, null);
    269             }
    270         }
    271 
    272         @Override
    273         public boolean needsFocus() {
    274             // Because we need to show the editor with text selected
    275             return true;
    276         }
    277 
    278         @Override
    279         public Image getImage() {
    280             ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    281             return sharedImages.getImage(ISharedImages.IMG_OBJ_ADD);
    282         }
    283     }
    284 
    285     private static final class AccessibilityFix extends SetPropertyFix {
    286         private AccessibilityFix(String id, IMarker marker) {
    287             super(id, marker);
    288         }
    289 
    290         @Override
    291         protected String getAttribute() {
    292             return ATTR_CONTENT_DESCRIPTION;
    293         }
    294 
    295         @Override
    296         public String getDisplayString() {
    297             return "Add content description attribute";
    298         }
    299     }
    300 
    301     private static final class SetInputTypeFix extends SetPropertyFix {
    302         private SetInputTypeFix(String id, IMarker marker) {
    303             super(id, marker);
    304         }
    305 
    306         @Override
    307         protected String getAttribute() {
    308             return ATTR_INPUT_TYPE;
    309         }
    310 
    311         @Override
    312         protected String getProposal() {
    313             return ""; //$NON-NLS-1$
    314         }
    315 
    316         @Override
    317         public String getDisplayString() {
    318             return "Set input type";
    319         }
    320 
    321         @Override
    322         public void apply(IDocument document) {
    323             super.apply(document);
    324             // Invoke code assist
    325             IEditorPart editor = AdtUtils.getActiveEditor();
    326             if (editor instanceof AndroidXmlEditor) {
    327                 ((AndroidXmlEditor) editor).invokeContentAssist(-1);
    328             }
    329         }
    330     }
    331 
    332     private static final class LinearLayoutWeightFix extends DocumentFix {
    333         private LinearLayoutWeightFix(String id, IMarker marker) {
    334             super(id, marker);
    335         }
    336 
    337         @Override
    338         public boolean needsFocus() {
    339             return false;
    340         }
    341 
    342         @Override
    343         protected void apply(IDocument document, IStructuredModel model, Node node, int start,
    344                 int end) {
    345             if (node instanceof Element && node.getParentNode() instanceof Element) {
    346                 Element element = (Element) node;
    347                 Element parent = (Element) node.getParentNode();
    348                 String dimension;
    349                 if (VALUE_VERTICAL.equals(parent.getAttributeNS(ANDROID_URI,
    350                         ATTR_ORIENTATION))) {
    351                     dimension = ATTR_LAYOUT_HEIGHT;
    352                 } else {
    353                     dimension = ATTR_LAYOUT_WIDTH;
    354                 }
    355                 element.setAttributeNS(ANDROID_URI, dimension, VALUE_ZERO_DP);
    356             }
    357         }
    358 
    359         @Override
    360         public String getDisplayString() {
    361             return "Replace size attribute with 0dp";
    362         }
    363 
    364         @Override
    365         public Image getImage() {
    366             ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    367             // TODO: Need a better icon here
    368             return sharedImages.getImage(ISharedImages.IMG_OBJ_ELEMENT);
    369         }
    370     }
    371 
    372     private static final class RemoveUselessViewFix extends DocumentFix {
    373         private RemoveUselessViewFix(String id, IMarker marker) {
    374             super(id, marker);
    375         }
    376 
    377         @Override
    378         public boolean needsFocus() {
    379             return mId.equals(mId.equals(UselessViewDetector.USELESS_PARENT.getId()));
    380         }
    381 
    382         @Override
    383         protected void apply(IDocument document, IStructuredModel model, Node node, int start,
    384                 int end) {
    385             if (node instanceof Element && node.getParentNode() instanceof Element) {
    386                 Element element = (Element) node;
    387                 Element parent = (Element) node.getParentNode();
    388 
    389                 if (mId.equals(UselessViewDetector.USELESS_LEAF.getId())) {
    390                     parent.removeChild(element);
    391                 } else {
    392                     assert mId.equals(UselessViewDetector.USELESS_PARENT.getId());
    393                     // Invoke refactoring
    394                     IEditorPart editor = AdtUtils.getActiveEditor();
    395                     if (editor instanceof LayoutEditor) {
    396                         LayoutEditor layout = (LayoutEditor) editor;
    397                         IFile file = (IFile) mMarker.getResource();
    398                         ITextSelection textSelection = new TextSelection(start,
    399                                 end - start);
    400                         UnwrapRefactoring refactoring =
    401                                 new UnwrapRefactoring(file, layout, textSelection, null);
    402                         RefactoringWizard wizard = refactoring.createWizard();
    403                         RefactoringWizardOpenOperation op =
    404                                 new RefactoringWizardOpenOperation(wizard);
    405                         try {
    406                             IWorkbenchWindow window = PlatformUI.getWorkbench().
    407                                     getActiveWorkbenchWindow();
    408                             op.run(window.getShell(), wizard.getDefaultPageTitle());
    409                         } catch (InterruptedException e) {
    410                         }
    411                     }
    412                 }
    413             }
    414         }
    415 
    416         @Override
    417         public String getDisplayString() {
    418             return "Remove unnecessary view";
    419         }
    420 
    421         @Override
    422         public Image getImage() {
    423             ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    424             return sharedImages.getImage(ISharedImages.IMG_ETOOL_DELETE);
    425         }
    426     }
    427 
    428     /**
    429      * Fix for extracting strings.
    430      * <p>
    431      * TODO: Look for existing string values, and if it matches one of the
    432      * existing Strings offer to just replace it with the given string!
    433      */
    434     private static final class ExtractStringFix extends DocumentFix {
    435         private ExtractStringFix(String id, IMarker marker) {
    436             super(id, marker);
    437         }
    438 
    439         @Override
    440         public boolean needsFocus() {
    441             return true;
    442         }
    443 
    444         @Override
    445         protected void apply(IDocument document, IStructuredModel model, Node node, int start,
    446                 int end) {
    447             // Invoke refactoring
    448             IEditorPart editor = AdtUtils.getActiveEditor();
    449             if (editor instanceof LayoutEditor) {
    450                 IFile file = (IFile) mMarker.getResource();
    451                 ITextSelection selection = new TextSelection(start,
    452                         end - start);
    453 
    454                 ExtractStringRefactoring refactoring = new ExtractStringRefactoring(file,
    455                         editor,
    456                         selection);
    457 
    458                 RefactoringWizard wizard = new ExtractStringWizard(refactoring,
    459                         file.getProject());
    460                 RefactoringWizardOpenOperation op = new RefactoringWizardOpenOperation(wizard);
    461                 try {
    462                     IWorkbenchWindow window = PlatformUI.getWorkbench().
    463                             getActiveWorkbenchWindow();
    464                     op.run(window.getShell(), wizard.getDefaultPageTitle());
    465                 } catch (InterruptedException e) {
    466                 }
    467             }
    468         }
    469 
    470         @Override
    471         public String getDisplayString() {
    472             return "Extract String";
    473         }
    474 
    475         @Override
    476         public Image getImage() {
    477             ISharedImages sharedImages = PlatformUI.getWorkbench().getSharedImages();
    478             return sharedImages.getImage(ISharedImages.IMG_OBJ_ADD);
    479         }
    480     }
    481 
    482     private static final class ConvertToDpFix extends DocumentFix implements IInputValidator {
    483         private ConvertToDpFix(String id, IMarker marker) {
    484             super(id, marker);
    485         }
    486 
    487         @Override
    488         public boolean needsFocus() {
    489             return false;
    490         }
    491 
    492         @Override
    493         protected void apply(IDocument document, IStructuredModel model, Node node, int start,
    494                 int end) {
    495             InputDialog d = new InputDialog(
    496                     AdtPlugin.getDisplay().getActiveShell(),
    497                     "Choose density",
    498                     "What is the screen density the current px value works with? (e.g. 160, 240, ...)",
    499                     "", //$NON-NLS-1$
    500                     this);
    501             if (d.open() == Window.OK) {
    502                 String dpiString = d.getValue();
    503                 Element element = (Element) node;
    504                 int dpi = Integer.parseInt(dpiString); // Already validated, won't throw exception
    505                 NamedNodeMap attributes = element.getAttributes();
    506                 Pattern pattern = Pattern.compile("(\\d+)px"); //$NON-NLS-1$
    507                 for (int i = 0, n = attributes.getLength(); i < n; i++) {
    508                     Attr attribute = (Attr) attributes.item(i);
    509                     String value = attribute.getValue();
    510                     if (value.endsWith("px")) {
    511                         Matcher matcher = pattern.matcher(value);
    512                         if (matcher.matches()) {
    513                             String numberString = matcher.group(1);
    514                             try {
    515                                 int px = Integer.parseInt(numberString);
    516                                 int dp = px * 160 / dpi;
    517                                 String newValue = String.format(VALUE_N_DP, dp);
    518                                 attribute.setNodeValue(newValue);
    519                             } catch (NumberFormatException nufe) {
    520                                 AdtPlugin.log(nufe, null);
    521                             }
    522                         }
    523                     }
    524                 }
    525             }
    526         }
    527 
    528         @Override
    529         public String getDisplayString() {
    530             return "Convert to \"dp\"...";
    531         }
    532 
    533         @Override
    534         public Image getImage() {
    535             return AdtPlugin.getAndroidLogo();
    536         }
    537 
    538         // ---- Implements IInputValidator ----
    539 
    540         public String isValid(String input) {
    541             if (input == null || input.length() == 0)
    542                 return " "; //$NON-NLS-1$
    543 
    544             try {
    545                 int i = Integer.parseInt(input);
    546                 if (i <= 0 || i > 1000) {
    547                     return "Invalid range";
    548                 }
    549             } catch (NumberFormatException x) {
    550                 return "Enter a valid number";
    551             }
    552 
    553             return null;
    554         }
    555     }
    556 
    557 
    558 }
    559