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