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.SdkConstants.DOT_JAR; 19 import static com.android.SdkConstants.DOT_XML; 20 import static com.android.SdkConstants.FD_NATIVE_LIBS; 21 import static com.android.ide.eclipse.adt.AdtConstants.MARKER_LINT; 22 import static com.android.ide.eclipse.adt.AdtUtils.workspacePathToFile; 23 24 import com.android.annotations.NonNull; 25 import com.android.annotations.Nullable; 26 import com.android.ide.eclipse.adt.AdtPlugin; 27 import com.android.ide.eclipse.adt.AdtUtils; 28 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 29 import com.android.ide.eclipse.adt.internal.editors.layout.uimodel.UiViewElementNode; 30 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 31 import com.android.ide.eclipse.adt.internal.project.BaseProjectHelper; 32 import com.android.ide.eclipse.adt.internal.sdk.Sdk; 33 import com.android.sdklib.IAndroidTarget; 34 import com.android.tools.lint.checks.BuiltinIssueRegistry; 35 import com.android.tools.lint.client.api.Configuration; 36 import com.android.tools.lint.client.api.IssueRegistry; 37 import com.android.tools.lint.client.api.JavaParser; 38 import com.android.tools.lint.client.api.LintClient; 39 import com.android.tools.lint.client.api.LintDriver; 40 import com.android.tools.lint.client.api.XmlParser; 41 import com.android.tools.lint.detector.api.ClassContext; 42 import com.android.tools.lint.detector.api.Context; 43 import com.android.tools.lint.detector.api.DefaultPosition; 44 import com.android.tools.lint.detector.api.Detector; 45 import com.android.tools.lint.detector.api.Issue; 46 import com.android.tools.lint.detector.api.JavaContext; 47 import com.android.tools.lint.detector.api.LintUtils; 48 import com.android.tools.lint.detector.api.Location; 49 import com.android.tools.lint.detector.api.Location.Handle; 50 import com.android.tools.lint.detector.api.Position; 51 import com.android.tools.lint.detector.api.Project; 52 import com.android.tools.lint.detector.api.Severity; 53 import com.android.tools.lint.detector.api.TextFormat; 54 import com.android.tools.lint.detector.api.XmlContext; 55 import com.android.utils.Pair; 56 import com.android.utils.SdkUtils; 57 import com.google.common.collect.Maps; 58 59 import org.eclipse.core.resources.IFile; 60 import org.eclipse.core.resources.IMarker; 61 import org.eclipse.core.resources.IProject; 62 import org.eclipse.core.resources.IResource; 63 import org.eclipse.core.runtime.CoreException; 64 import org.eclipse.core.runtime.IStatus; 65 import org.eclipse.core.runtime.NullProgressMonitor; 66 import org.eclipse.jdt.core.IClasspathEntry; 67 import org.eclipse.jdt.core.IJavaProject; 68 import org.eclipse.jdt.core.IType; 69 import org.eclipse.jdt.core.ITypeHierarchy; 70 import org.eclipse.jdt.core.JavaCore; 71 import org.eclipse.jdt.core.JavaModelException; 72 import org.eclipse.jdt.internal.compiler.CompilationResult; 73 import org.eclipse.jdt.internal.compiler.DefaultErrorHandlingPolicies; 74 import org.eclipse.jdt.internal.compiler.ast.CompilationUnitDeclaration; 75 import org.eclipse.jdt.internal.compiler.batch.CompilationUnit; 76 import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants; 77 import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; 78 import org.eclipse.jdt.internal.compiler.parser.Parser; 79 import org.eclipse.jdt.internal.compiler.problem.AbortCompilation; 80 import org.eclipse.jdt.internal.compiler.problem.DefaultProblemFactory; 81 import org.eclipse.jdt.internal.compiler.problem.ProblemReporter; 82 import org.eclipse.jface.text.BadLocationException; 83 import org.eclipse.jface.text.IDocument; 84 import org.eclipse.jface.text.IRegion; 85 import org.eclipse.swt.widgets.Shell; 86 import org.eclipse.ui.IEditorPart; 87 import org.eclipse.ui.PartInitException; 88 import org.eclipse.ui.editors.text.TextFileDocumentProvider; 89 import org.eclipse.ui.ide.IDE; 90 import org.eclipse.ui.texteditor.IDocumentProvider; 91 import org.eclipse.wst.sse.core.StructuredModelManager; 92 import org.eclipse.wst.sse.core.internal.provisional.IModelManager; 93 import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; 94 import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; 95 import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; 96 import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; 97 import org.w3c.dom.Attr; 98 import org.w3c.dom.Document; 99 import org.w3c.dom.Node; 100 101 import java.io.File; 102 import java.io.IOException; 103 import java.util.ArrayList; 104 import java.util.Collection; 105 import java.util.Collections; 106 import java.util.List; 107 import java.util.Map; 108 import java.util.WeakHashMap; 109 110 import lombok.ast.ecj.EcjTreeConverter; 111 import lombok.ast.grammar.ParseProblem; 112 import lombok.ast.grammar.Source; 113 114 /** 115 * Eclipse implementation for running lint on workspace files and projects. 116 */ 117 @SuppressWarnings("restriction") // DOM model 118 public class EclipseLintClient extends LintClient { 119 static final String MARKER_CHECKID_PROPERTY = "checkid"; //$NON-NLS-1$ 120 private static final String MODEL_PROPERTY = "model"; //$NON-NLS-1$ 121 private final List<? extends IResource> mResources; 122 private final IDocument mDocument; 123 private boolean mWasFatal; 124 private boolean mFatalOnly; 125 private EclipseJavaParser mJavaParser; 126 private boolean mCollectNodes; 127 private Map<Node, IMarker> mNodeMap; 128 129 /** 130 * Creates a new {@link EclipseLintClient}. 131 * 132 * @param registry the associated detector registry 133 * @param resources the associated resources (project, file or null) 134 * @param document the associated document, or null if the {@code resource} 135 * param is not a file 136 * @param fatalOnly whether only fatal issues should be reported (and therefore checked) 137 */ 138 public EclipseLintClient(IssueRegistry registry, List<? extends IResource> resources, 139 IDocument document, boolean fatalOnly) { 140 mResources = resources; 141 mDocument = document; 142 mFatalOnly = fatalOnly; 143 } 144 145 /** 146 * Returns true if lint should only check fatal issues 147 * 148 * @return true if lint should only check fatal issues 149 */ 150 public boolean isFatalOnly() { 151 return mFatalOnly; 152 } 153 154 /** 155 * Sets whether the lint client should store associated XML nodes for each 156 * reported issue 157 * 158 * @param collectNodes if true, collect node positions for errors in XML 159 * files, retrievable via the {@link #getIssueForNode} method 160 */ 161 public void setCollectNodes(boolean collectNodes) { 162 mCollectNodes = collectNodes; 163 } 164 165 /** 166 * Returns one of the issues for the given node (there could be more than one) 167 * 168 * @param node the node to look up lint issues for 169 * @return the marker for one of the issues found for the given node 170 */ 171 @Nullable 172 public IMarker getIssueForNode(@NonNull UiViewElementNode node) { 173 if (mNodeMap != null) { 174 return mNodeMap.get(node.getXmlNode()); 175 } 176 177 return null; 178 } 179 180 /** 181 * Returns a collection of nodes that have one or more lint warnings 182 * associated with them (retrievable via 183 * {@link #getIssueForNode(UiViewElementNode)}) 184 * 185 * @return a collection of nodes, which should <b>not</b> be modified by the 186 * caller 187 */ 188 @Nullable 189 public Collection<Node> getIssueNodes() { 190 if (mNodeMap != null) { 191 return mNodeMap.keySet(); 192 } 193 194 return null; 195 } 196 197 // ----- Extends LintClient ----- 198 199 @Override 200 public void log(@NonNull Severity severity, @Nullable Throwable exception, 201 @Nullable String format, @Nullable Object... args) { 202 if (exception == null) { 203 AdtPlugin.log(IStatus.WARNING, format, args); 204 } else { 205 AdtPlugin.log(exception, format, args); 206 } 207 } 208 209 @Override 210 public XmlParser getXmlParser() { 211 return new XmlParser() { 212 @Override 213 public Document parseXml(@NonNull XmlContext context) { 214 // Map File to IFile 215 IFile file = AdtUtils.fileToIFile(context.file); 216 if (file == null || !file.exists()) { 217 String path = context.file.getPath(); 218 AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path); 219 return null; 220 } 221 222 IStructuredModel model = null; 223 try { 224 IModelManager modelManager = StructuredModelManager.getModelManager(); 225 if (modelManager == null) { 226 // This can happen if incremental lint is running right as Eclipse is 227 // shutting down 228 return null; 229 } 230 model = modelManager.getModelForRead(file); 231 if (model instanceof IDOMModel) { 232 context.setProperty(MODEL_PROPERTY, model); 233 IDOMModel domModel = (IDOMModel) model; 234 return domModel.getDocument(); 235 } 236 } catch (IOException e) { 237 AdtPlugin.log(e, "Cannot read XML file"); 238 } catch (CoreException e) { 239 AdtPlugin.log(e, null); 240 } 241 242 return null; 243 } 244 245 @Override 246 public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node) { 247 IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); 248 return new LazyLocation(context.file, model.getStructuredDocument(), 249 (IndexedRegion) node); 250 } 251 252 @Override 253 public @NonNull Location getLocation(@NonNull XmlContext context, @NonNull Node node, 254 int start, int end) { 255 IndexedRegion region = (IndexedRegion) node; 256 int nodeStart = region.getStartOffset(); 257 258 IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); 259 // Get line number 260 LazyLocation location = new LazyLocation(context.file, 261 model.getStructuredDocument(), region); 262 int line = location.getStart().getLine(); 263 264 Position startPos = new DefaultPosition(line, -1, nodeStart + start); 265 Position endPos = new DefaultPosition(line, -1, nodeStart + end); 266 return Location.create(context.file, startPos, endPos); 267 } 268 269 @Override 270 public int getNodeStartOffset(@NonNull XmlContext context, @NonNull Node node) { 271 IndexedRegion region = (IndexedRegion) node; 272 return region.getStartOffset(); 273 } 274 275 @Override 276 public int getNodeEndOffset(@NonNull XmlContext context, @NonNull Node node) { 277 IndexedRegion region = (IndexedRegion) node; 278 return region.getEndOffset(); 279 } 280 281 @Override 282 public @NonNull Handle createLocationHandle(final @NonNull XmlContext context, 283 final @NonNull Node node) { 284 IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); 285 return new LazyLocation(context.file, model.getStructuredDocument(), 286 (IndexedRegion) node); 287 } 288 289 @Override 290 public void dispose(@NonNull XmlContext context, @NonNull Document document) { 291 IStructuredModel model = (IStructuredModel) context.getProperty(MODEL_PROPERTY); 292 assert model != null : context.file; 293 if (model != null) { 294 model.releaseFromRead(); 295 } 296 } 297 298 @Override 299 @NonNull 300 public Location getNameLocation(@NonNull XmlContext context, @NonNull Node node) { 301 return getLocation(context, node); 302 } 303 304 @Override 305 @NonNull 306 public Location getValueLocation(@NonNull XmlContext context, @NonNull Attr node) { 307 return getLocation(context, node); 308 } 309 310 }; 311 } 312 313 @Override 314 public JavaParser getJavaParser(@Nullable Project project) { 315 if (mJavaParser == null) { 316 mJavaParser = new EclipseJavaParser(); 317 } 318 319 return mJavaParser; 320 } 321 322 // Cache for {@link getProject} 323 private IProject mLastEclipseProject; 324 private Project mLastLintProject; 325 326 private IProject getProject(Project project) { 327 if (project == mLastLintProject) { 328 return mLastEclipseProject; 329 } 330 331 mLastLintProject = project; 332 mLastEclipseProject = null; 333 334 if (mResources != null) { 335 if (mResources.size() == 1) { 336 IProject p = mResources.get(0).getProject(); 337 mLastEclipseProject = p; 338 return p; 339 } 340 341 IProject last = null; 342 for (IResource resource : mResources) { 343 IProject p = resource.getProject(); 344 if (p != last) { 345 if (project.getDir().equals(AdtUtils.getAbsolutePath(p).toFile())) { 346 mLastEclipseProject = p; 347 return p; 348 } 349 last = p; 350 } 351 } 352 } 353 354 return null; 355 } 356 357 @Override 358 @NonNull 359 public String getProjectName(@NonNull Project project) { 360 // Initialize the lint project's name to the name of the Eclipse project, 361 // which might differ from the directory name 362 IProject eclipseProject = getProject(project); 363 if (eclipseProject != null) { 364 return eclipseProject.getName(); 365 } 366 367 return super.getProjectName(project); 368 } 369 370 @NonNull 371 @Override 372 public Configuration getConfiguration(@NonNull Project project, @Nullable LintDriver driver) { 373 return getConfigurationFor(project); 374 } 375 376 /** 377 * Same as {@link #getConfiguration(Project)}, but {@code project} can be 378 * null in which case the global configuration is returned. 379 * 380 * @param project the project to look up 381 * @return a corresponding configuration 382 */ 383 @NonNull 384 public Configuration getConfigurationFor(@Nullable Project project) { 385 if (project != null) { 386 IProject eclipseProject = getProject(project); 387 if (eclipseProject != null) { 388 return ProjectLintConfiguration.get(this, eclipseProject, mFatalOnly); 389 } 390 } 391 392 return GlobalLintConfiguration.get(); 393 } 394 @Override 395 public void report(@NonNull Context context, @NonNull Issue issue, @NonNull Severity s, 396 @Nullable Location location, 397 @NonNull String message, @NonNull TextFormat format) { 398 message = format.toText(message); 399 int severity = getMarkerSeverity(s); 400 IMarker marker = null; 401 if (location != null) { 402 Position startPosition = location.getStart(); 403 if (startPosition == null) { 404 if (location.getFile() != null) { 405 IResource resource = AdtUtils.fileToResource(location.getFile()); 406 if (resource != null && resource.isAccessible()) { 407 marker = BaseProjectHelper.markResource(resource, MARKER_LINT, 408 message, 0, severity); 409 } 410 } 411 } else { 412 Position endPosition = location.getEnd(); 413 int line = startPosition.getLine() + 1; // Marker API is 1-based 414 IFile file = AdtUtils.fileToIFile(location.getFile()); 415 if (file != null && file.isAccessible()) { 416 Pair<Integer, Integer> r = getRange(file, mDocument, 417 startPosition, endPosition); 418 int startOffset = r.getFirst(); 419 int endOffset = r.getSecond(); 420 marker = BaseProjectHelper.markResource(file, MARKER_LINT, 421 message, line, startOffset, endOffset, severity); 422 } 423 } 424 } 425 426 if (marker == null) { 427 marker = BaseProjectHelper.markResource(mResources.get(0), MARKER_LINT, 428 message, 0, severity); 429 } 430 431 if (marker != null) { 432 // Store marker id such that we can recognize it from the suppress quickfix 433 try { 434 marker.setAttribute(MARKER_CHECKID_PROPERTY, issue.getId()); 435 } catch (CoreException e) { 436 AdtPlugin.log(e, null); 437 } 438 } 439 440 if (s == Severity.FATAL) { 441 mWasFatal = true; 442 } 443 444 if (mCollectNodes && location != null && marker != null) { 445 if (location instanceof LazyLocation) { 446 LazyLocation l = (LazyLocation) location; 447 IndexedRegion region = l.mRegion; 448 if (region instanceof Node) { 449 Node node = (Node) region; 450 if (node instanceof Attr) { 451 node = ((Attr) node).getOwnerElement(); 452 } 453 if (mNodeMap == null) { 454 mNodeMap = new WeakHashMap<Node, IMarker>(); 455 } 456 IMarker prev = mNodeMap.get(node); 457 if (prev != null) { 458 // Only replace the node if this node has higher priority 459 int prevSeverity = prev.getAttribute(IMarker.SEVERITY, 0); 460 if (prevSeverity < severity) { 461 mNodeMap.put(node, marker); 462 } 463 } else { 464 mNodeMap.put(node, marker); 465 } 466 } 467 } 468 } 469 } 470 471 @Override 472 @Nullable 473 public File findResource(@NonNull String relativePath) { 474 // Look within the $ANDROID_SDK 475 String sdkFolder = AdtPrefs.getPrefs().getOsSdkFolder(); 476 if (sdkFolder != null) { 477 File file = new File(sdkFolder, relativePath); 478 if (file.exists()) { 479 return file; 480 } 481 } 482 483 return null; 484 } 485 486 /** 487 * Clears any lint markers from the given resource (project, folder or file) 488 * 489 * @param resource the resource to remove markers from 490 */ 491 public static void clearMarkers(@NonNull IResource resource) { 492 clearMarkers(Collections.singletonList(resource)); 493 } 494 495 /** Clears any lint markers from the given list of resource (project, folder or file) */ 496 static void clearMarkers(List<? extends IResource> resources) { 497 for (IResource resource : resources) { 498 try { 499 if (resource.isAccessible()) { 500 resource.deleteMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE); 501 } 502 } catch (CoreException e) { 503 AdtPlugin.log(e, null); 504 } 505 } 506 507 IEditorPart activeEditor = AdtUtils.getActiveEditor(); 508 LayoutEditorDelegate delegate = LayoutEditorDelegate.fromEditor(activeEditor); 509 if (delegate != null) { 510 delegate.getGraphicalEditor().getLayoutActionBar().updateErrorIndicator(); 511 } 512 } 513 514 /** 515 * Removes all markers of the given id from the given resource. 516 * 517 * @param resource the resource to remove markers from (file or project, or 518 * null for all open projects) 519 * @param id the id for the issue whose markers should be deleted 520 */ 521 public static void removeMarkers(IResource resource, String id) { 522 if (resource == null) { 523 IJavaProject[] androidProjects = BaseProjectHelper.getAndroidProjects(null); 524 for (IJavaProject project : androidProjects) { 525 IProject p = project.getProject(); 526 if (p != null) { 527 // Recurse, but with a different parameter so it will not continue recursing 528 removeMarkers(p, id); 529 } 530 } 531 return; 532 } 533 IMarker[] markers = getMarkers(resource); 534 for (IMarker marker : markers) { 535 if (id.equals(getId(marker))) { 536 try { 537 marker.delete(); 538 } catch (CoreException e) { 539 AdtPlugin.log(e, null); 540 } 541 } 542 } 543 } 544 545 /** 546 * Returns the lint marker for the given resource (which may be a project, folder or file) 547 * 548 * @param resource the resource to be checked, typically a source file 549 * @return an array of markers, possibly empty but never null 550 */ 551 public static IMarker[] getMarkers(IResource resource) { 552 try { 553 if (resource.isAccessible()) { 554 return resource.findMarkers(MARKER_LINT, false, IResource.DEPTH_INFINITE); 555 } 556 } catch (CoreException e) { 557 AdtPlugin.log(e, null); 558 } 559 560 return new IMarker[0]; 561 } 562 563 private static int getMarkerSeverity(Severity severity) { 564 switch (severity) { 565 case INFORMATIONAL: 566 return IMarker.SEVERITY_INFO; 567 case WARNING: 568 return IMarker.SEVERITY_WARNING; 569 case FATAL: 570 case ERROR: 571 default: 572 return IMarker.SEVERITY_ERROR; 573 } 574 } 575 576 private static Pair<Integer, Integer> getRange(IFile file, IDocument doc, 577 Position startPosition, Position endPosition) { 578 int startOffset = startPosition.getOffset(); 579 int endOffset = endPosition != null ? endPosition.getOffset() : -1; 580 if (endOffset != -1) { 581 // Attribute ranges often include trailing whitespace; trim this up 582 if (doc == null) { 583 IDocumentProvider provider = new TextFileDocumentProvider(); 584 try { 585 provider.connect(file); 586 doc = provider.getDocument(file); 587 if (doc != null) { 588 return adjustOffsets(doc, startOffset, endOffset); 589 } 590 } catch (Exception e) { 591 AdtPlugin.log(e, "Can't find range information for %1$s", file.getName()); 592 } finally { 593 provider.disconnect(file); 594 } 595 } else { 596 return adjustOffsets(doc, startOffset, endOffset); 597 } 598 } 599 600 return Pair.of(startOffset, startOffset); 601 } 602 603 /** 604 * Trim off any trailing space on the given offset range in the given 605 * document, and don't span multiple lines on ranges since it makes (for 606 * example) the XML editor just glow with yellow underlines for all the 607 * attributes etc. Highlighting just the element beginning gets the point 608 * across. It also makes it more obvious where there are warnings on both 609 * the overall element and on individual attributes since without this the 610 * warnings on attributes would just overlap with the whole-element 611 * highlighting. 612 */ 613 private static Pair<Integer, Integer> adjustOffsets(IDocument doc, int startOffset, 614 int endOffset) { 615 int originalStart = startOffset; 616 int originalEnd = endOffset; 617 618 if (doc != null) { 619 while (endOffset > startOffset && endOffset < doc.getLength()) { 620 try { 621 if (!Character.isWhitespace(doc.getChar(endOffset - 1))) { 622 break; 623 } else { 624 endOffset--; 625 } 626 } catch (BadLocationException e) { 627 // Pass - we've already validated offset range above 628 break; 629 } 630 } 631 632 // Also don't span lines 633 int lineEnd = startOffset; 634 while (lineEnd < endOffset) { 635 try { 636 char c = doc.getChar(lineEnd); 637 if (c == '\n' || c == '\r') { 638 endOffset = lineEnd; 639 if (endOffset > 0 && doc.getChar(endOffset - 1) == '\r') { 640 endOffset--; 641 } 642 break; 643 } 644 } catch (BadLocationException e) { 645 // Pass - we've already validated offset range above 646 break; 647 } 648 lineEnd++; 649 } 650 } 651 652 if (startOffset >= endOffset) { 653 // Selecting nothing (for example, for the mangled CRLF delimiter issue selecting 654 // just the newline) 655 // In that case, use the real range 656 return Pair.of(originalStart, originalEnd); 657 } 658 659 return Pair.of(startOffset, endOffset); 660 } 661 662 /** 663 * Returns true if a fatal error was encountered 664 * 665 * @return true if a fatal error was encountered 666 */ 667 public boolean hasFatalErrors() { 668 return mWasFatal; 669 } 670 671 /** 672 * Describe the issue for the given marker 673 * 674 * @param marker the marker to look up 675 * @return a full description of the corresponding issue, never null 676 */ 677 public static String describe(IMarker marker) { 678 IssueRegistry registry = getRegistry(); 679 String markerId = getId(marker); 680 Issue issue = registry.getIssue(markerId); 681 if (issue == null) { 682 return ""; 683 } 684 685 String summary = issue.getBriefDescription(TextFormat.TEXT); 686 String explanation = issue.getExplanation(TextFormat.TEXT); 687 688 StringBuilder sb = new StringBuilder(summary.length() + explanation.length() + 20); 689 try { 690 sb.append((String) marker.getAttribute(IMarker.MESSAGE)); 691 sb.append('\n').append('\n'); 692 } catch (CoreException e) { 693 } 694 sb.append("Issue: "); 695 sb.append(summary); 696 sb.append('\n'); 697 sb.append("Id: "); 698 sb.append(issue.getId()); 699 sb.append('\n').append('\n'); 700 sb.append(explanation); 701 702 if (issue.getMoreInfo() != null) { 703 sb.append('\n').append('\n'); 704 sb.append(issue.getMoreInfo()); 705 } 706 707 return sb.toString(); 708 } 709 710 /** 711 * Returns the id for the given marker 712 * 713 * @param marker the marker to look up 714 * @return the corresponding issue id, or null 715 */ 716 public static String getId(IMarker marker) { 717 try { 718 return (String) marker.getAttribute(MARKER_CHECKID_PROPERTY); 719 } catch (CoreException e) { 720 return null; 721 } 722 } 723 724 /** 725 * Shows the given marker in the editor 726 * 727 * @param marker the marker to be shown 728 */ 729 public static void showMarker(IMarker marker) { 730 IRegion region = null; 731 try { 732 int start = marker.getAttribute(IMarker.CHAR_START, -1); 733 int end = marker.getAttribute(IMarker.CHAR_END, -1); 734 if (start >= 0 && end >= 0) { 735 region = new org.eclipse.jface.text.Region(start, end - start); 736 } 737 738 IResource resource = marker.getResource(); 739 if (resource instanceof IFile) { 740 IEditorPart editor = 741 AdtPlugin.openFile((IFile) resource, region, true /* showEditorTab */); 742 if (editor != null) { 743 IDE.gotoMarker(editor, marker); 744 } 745 } 746 } catch (PartInitException ex) { 747 AdtPlugin.log(ex, null); 748 } 749 } 750 751 /** 752 * Show a dialog with errors for the given file 753 * 754 * @param shell the parent shell to attach the dialog to 755 * @param file the file to show the errors for 756 * @param editor the editor for the file, if known 757 */ 758 public static void showErrors( 759 @NonNull Shell shell, 760 @NonNull IFile file, 761 @Nullable IEditorPart editor) { 762 LintListDialog dialog = new LintListDialog(shell, file, editor); 763 dialog.open(); 764 } 765 766 @Override 767 public @NonNull String readFile(@NonNull File f) { 768 // Map File to IFile 769 IFile file = AdtUtils.fileToIFile(f); 770 if (file == null || !file.exists()) { 771 String path = f.getPath(); 772 AdtPlugin.log(IStatus.ERROR, "Can't find file %1$s in workspace", path); 773 return readPlainFile(f); 774 } 775 776 if (SdkUtils.endsWithIgnoreCase(file.getName(), DOT_XML)) { 777 IStructuredModel model = null; 778 try { 779 IModelManager modelManager = StructuredModelManager.getModelManager(); 780 model = modelManager.getModelForRead(file); 781 return model.getStructuredDocument().get(); 782 } catch (IOException e) { 783 AdtPlugin.log(e, "Cannot read XML file"); 784 } catch (CoreException e) { 785 AdtPlugin.log(e, null); 786 } finally { 787 if (model != null) { 788 // TODO: This may be too early... 789 model.releaseFromRead(); 790 } 791 } 792 } 793 794 return readPlainFile(f); 795 } 796 797 private String readPlainFile(File file) { 798 try { 799 return LintUtils.getEncodedString(this, file); 800 } catch (IOException e) { 801 return ""; //$NON-NLS-1$ 802 } 803 } 804 805 private Map<Project, ClassPathInfo> mProjectInfo; 806 807 @Override 808 @NonNull 809 protected ClassPathInfo getClassPath(@NonNull Project project) { 810 ClassPathInfo info; 811 if (mProjectInfo == null) { 812 mProjectInfo = Maps.newHashMap(); 813 info = null; 814 } else { 815 info = mProjectInfo.get(project); 816 } 817 818 if (info == null) { 819 List<File> sources = null; 820 List<File> classes = null; 821 List<File> libraries = null; 822 823 IProject p = getProject(project); 824 if (p != null) { 825 try { 826 IJavaProject javaProject = BaseProjectHelper.getJavaProject(p); 827 828 // Output path 829 File file = workspacePathToFile(javaProject.getOutputLocation()); 830 classes = Collections.singletonList(file); 831 832 // Source path 833 IClasspathEntry[] entries = javaProject.getRawClasspath(); 834 sources = new ArrayList<File>(entries.length); 835 libraries = new ArrayList<File>(entries.length); 836 for (int i = 0; i < entries.length; i++) { 837 IClasspathEntry entry = entries[i]; 838 int kind = entry.getEntryKind(); 839 840 if (kind == IClasspathEntry.CPE_VARIABLE) { 841 entry = JavaCore.getResolvedClasspathEntry(entry); 842 if (entry == null) { 843 // It's possible that the variable is no longer valid; ignore 844 continue; 845 } 846 kind = entry.getEntryKind(); 847 } 848 849 if (kind == IClasspathEntry.CPE_SOURCE) { 850 sources.add(workspacePathToFile(entry.getPath())); 851 } else if (kind == IClasspathEntry.CPE_LIBRARY) { 852 libraries.add(entry.getPath().toFile()); 853 } 854 // Note that we ignore IClasspathEntry.CPE_CONTAINER: 855 // Normal Android Eclipse projects supply both 856 // AdtConstants.CONTAINER_FRAMEWORK 857 // and 858 // AdtConstants.CONTAINER_LIBRARIES 859 // here. We ignore the framework classes for obvious reasons, 860 // but we also ignore the library container because lint will 861 // process the libraries differently. When Eclipse builds a 862 // project, it gets the .jar output of the library projects 863 // from this container, which means it doesn't have to process 864 // the library sources. Lint on the other hand wants to process 865 // the source code, so instead it actually looks at the 866 // project.properties file to find the libraries, and then it 867 // iterates over all the library projects in turn and analyzes 868 // those separately (but passing the main project for context, 869 // such that the including project's manifest declarations 870 // are used for data like minSdkVersion level). 871 // 872 // Note that this container will also contain *other* 873 // libraries (Java libraries, not library projects) that we 874 // *should* include. However, we can't distinguish these 875 // class path entries from the library project jars, 876 // so instead of looking at these, we simply listFiles() in 877 // the libs/ folder after processing the classpath info 878 } 879 880 // Add in libraries 881 File libs = new File(project.getDir(), FD_NATIVE_LIBS); 882 if (libs.isDirectory()) { 883 File[] jars = libs.listFiles(); 884 if (jars != null) { 885 for (File jar : jars) { 886 if (SdkUtils.endsWith(jar.getPath(), DOT_JAR)) { 887 libraries.add(jar); 888 } 889 } 890 } 891 } 892 } catch (CoreException e) { 893 AdtPlugin.log(e, null); 894 } 895 } 896 897 if (sources == null) { 898 sources = super.getClassPath(project).getSourceFolders(); 899 } 900 if (classes == null) { 901 classes = super.getClassPath(project).getClassFolders(); 902 } 903 if (libraries == null) { 904 libraries = super.getClassPath(project).getLibraries(); 905 } 906 907 908 // No test folders in Eclipse: 909 // https://bugs.eclipse.org/bugs/show_bug.cgi?id=224708 910 List<File> tests = Collections.emptyList(); 911 912 info = new ClassPathInfo(sources, classes, libraries, tests); 913 mProjectInfo.put(project, info); 914 } 915 916 return info; 917 } 918 919 /** 920 * Returns the registry of issues to check from within Eclipse. 921 * 922 * @return the issue registry to use to access detectors and issues 923 */ 924 public static IssueRegistry getRegistry() { 925 return new EclipseLintIssueRegistry(); 926 } 927 928 @Override 929 public @NonNull Class<? extends Detector> replaceDetector( 930 @NonNull Class<? extends Detector> detectorClass) { 931 return detectorClass; 932 } 933 934 @Override 935 @NonNull 936 public IAndroidTarget[] getTargets() { 937 Sdk sdk = Sdk.getCurrent(); 938 if (sdk != null) { 939 return sdk.getTargets(); 940 } else { 941 return new IAndroidTarget[0]; 942 } 943 } 944 945 private boolean mSearchForSuperClasses; 946 947 /** 948 * Sets whether this client should search for super types on its own. This 949 * is typically not needed when doing a full lint run (because lint will 950 * look at all classes and libraries), but is useful during incremental 951 * analysis when lint is only looking at a subset of classes. In that case, 952 * we want to use Eclipse's data structures for super classes. 953 * 954 * @param search whether to use a custom Eclipse search for super class 955 * names 956 */ 957 public void setSearchForSuperClasses(boolean search) { 958 mSearchForSuperClasses = search; 959 } 960 961 /** 962 * Whether this lint client is searching for super types. See 963 * {@link #setSearchForSuperClasses(boolean)} for details. 964 * 965 * @return whether the client will search for super types 966 */ 967 public boolean getSearchForSuperClasses() { 968 return mSearchForSuperClasses; 969 } 970 971 @Override 972 @Nullable 973 public String getSuperClass(@NonNull Project project, @NonNull String name) { 974 if (!mSearchForSuperClasses) { 975 // Super type search using the Eclipse index is potentially slow, so 976 // only do this when necessary 977 return null; 978 } 979 980 IProject eclipseProject = getProject(project); 981 if (eclipseProject == null) { 982 return null; 983 } 984 985 try { 986 IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); 987 if (javaProject == null) { 988 return null; 989 } 990 991 String typeFqcn = ClassContext.getFqcn(name); 992 IType type = javaProject.findType(typeFqcn); 993 if (type != null) { 994 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); 995 IType superType = hierarchy.getSuperclass(type); 996 if (superType != null) { 997 String key = superType.getKey(); 998 if (!key.isEmpty() 999 && key.charAt(0) == 'L' 1000 && key.charAt(key.length() - 1) == ';') { 1001 return key.substring(1, key.length() - 1); 1002 } else { 1003 String fqcn = superType.getFullyQualifiedName(); 1004 return ClassContext.getInternalName(fqcn); 1005 } 1006 } 1007 } 1008 } catch (JavaModelException e) { 1009 log(Severity.INFORMATIONAL, e, null); 1010 } catch (CoreException e) { 1011 log(Severity.INFORMATIONAL, e, null); 1012 } 1013 1014 return null; 1015 } 1016 1017 @Override 1018 @Nullable 1019 public Boolean isSubclassOf( 1020 @NonNull Project project, 1021 @NonNull String name, @NonNull 1022 String superClassName) { 1023 if (!mSearchForSuperClasses) { 1024 // Super type search using the Eclipse index is potentially slow, so 1025 // only do this when necessary 1026 return null; 1027 } 1028 1029 IProject eclipseProject = getProject(project); 1030 if (eclipseProject == null) { 1031 return null; 1032 } 1033 1034 try { 1035 IJavaProject javaProject = BaseProjectHelper.getJavaProject(eclipseProject); 1036 if (javaProject == null) { 1037 return null; 1038 } 1039 1040 String typeFqcn = ClassContext.getFqcn(name); 1041 IType type = javaProject.findType(typeFqcn); 1042 if (type != null) { 1043 ITypeHierarchy hierarchy = type.newSupertypeHierarchy(new NullProgressMonitor()); 1044 IType[] allSupertypes = hierarchy.getAllSuperclasses(type); 1045 if (allSupertypes != null) { 1046 String target = 'L' + superClassName + ';'; 1047 for (IType superType : allSupertypes) { 1048 if (target.equals(superType.getKey())) { 1049 return Boolean.TRUE; 1050 } 1051 } 1052 return Boolean.FALSE; 1053 } 1054 } 1055 } catch (JavaModelException e) { 1056 log(Severity.INFORMATIONAL, e, null); 1057 } catch (CoreException e) { 1058 log(Severity.INFORMATIONAL, e, null); 1059 } 1060 1061 return null; 1062 } 1063 1064 private static class LazyLocation extends Location implements Location.Handle { 1065 private final IStructuredDocument mDocument; 1066 private final IndexedRegion mRegion; 1067 private Position mStart; 1068 private Position mEnd; 1069 1070 public LazyLocation(File file, IStructuredDocument document, IndexedRegion region) { 1071 super(file, null /*start*/, null /*end*/); 1072 mDocument = document; 1073 mRegion = region; 1074 } 1075 1076 @Override 1077 public Position getStart() { 1078 if (mStart == null) { 1079 int line = -1; 1080 int column = -1; 1081 int offset = mRegion.getStartOffset(); 1082 1083 if (mRegion instanceof org.w3c.dom.Text && mDocument != null) { 1084 // For text nodes, skip whitespace prefix, if any 1085 for (int i = offset; 1086 i < mRegion.getEndOffset() && i < mDocument.getLength(); i++) { 1087 try { 1088 char c = mDocument.getChar(i); 1089 if (!Character.isWhitespace(c)) { 1090 offset = i; 1091 break; 1092 } 1093 } catch (BadLocationException e) { 1094 break; 1095 } 1096 } 1097 } 1098 1099 if (mDocument != null && offset < mDocument.getLength()) { 1100 line = mDocument.getLineOfOffset(offset); 1101 column = -1; 1102 try { 1103 int lineOffset = mDocument.getLineOffset(line); 1104 column = offset - lineOffset; 1105 } catch (BadLocationException e) { 1106 AdtPlugin.log(e, null); 1107 } 1108 } 1109 1110 mStart = new DefaultPosition(line, column, offset); 1111 } 1112 1113 return mStart; 1114 } 1115 1116 @Override 1117 public Position getEnd() { 1118 if (mEnd == null) { 1119 mEnd = new DefaultPosition(-1, -1, mRegion.getEndOffset()); 1120 } 1121 1122 return mEnd; 1123 } 1124 1125 @Override 1126 public @NonNull Location resolve() { 1127 return this; 1128 } 1129 } 1130 1131 private static class EclipseJavaParser extends JavaParser { 1132 private static final boolean USE_ECLIPSE_PARSER = true; 1133 private final Parser mParser; 1134 1135 EclipseJavaParser() { 1136 if (USE_ECLIPSE_PARSER) { 1137 CompilerOptions options = new CompilerOptions(); 1138 // Always using JDK 7 rather than basing it on project metadata since we 1139 // don't do compilation error validation in lint (we leave that to the IDE's 1140 // error parser or the command line build's compilation step); we want an 1141 // AST that is as tolerant as possible. 1142 options.complianceLevel = ClassFileConstants.JDK1_7; 1143 options.sourceLevel = ClassFileConstants.JDK1_7; 1144 options.targetJDK = ClassFileConstants.JDK1_7; 1145 options.parseLiteralExpressionsAsConstants = true; 1146 ProblemReporter problemReporter = new ProblemReporter( 1147 DefaultErrorHandlingPolicies.exitOnFirstError(), 1148 options, 1149 new DefaultProblemFactory()); 1150 mParser = new Parser(problemReporter, options.parseLiteralExpressionsAsConstants); 1151 mParser.javadocParser.checkDocComment = false; 1152 } else { 1153 mParser = null; 1154 } 1155 } 1156 1157 @Override 1158 public void prepareJavaParse(@NonNull List<JavaContext> contexts) { 1159 // TODO: Use batch compiler from lint-cli.jar 1160 } 1161 1162 @Override 1163 public lombok.ast.Node parseJava(@NonNull JavaContext context) { 1164 if (USE_ECLIPSE_PARSER) { 1165 // Use Eclipse's compiler 1166 EcjTreeConverter converter = new EcjTreeConverter(); 1167 String code = context.getContents(); 1168 1169 CompilationUnit sourceUnit = new CompilationUnit(code.toCharArray(), 1170 context.file.getName(), "UTF-8"); //$NON-NLS-1$ 1171 CompilationResult compilationResult = new CompilationResult(sourceUnit, 0, 0, 0); 1172 CompilationUnitDeclaration unit = null; 1173 try { 1174 unit = mParser.parse(sourceUnit, compilationResult); 1175 } catch (AbortCompilation e) { 1176 // No need to report Java parsing errors while running in Eclipse. 1177 // Eclipse itself will already provide problem markers for these files, 1178 // so all this achieves is creating "multiple annotations on this line" 1179 // tooltips instead. 1180 return null; 1181 } 1182 if (unit == null) { 1183 return null; 1184 } 1185 1186 try { 1187 converter.visit(code, unit); 1188 List<? extends lombok.ast.Node> nodes = converter.getAll(); 1189 1190 // There could be more than one node when there are errors; pick out the 1191 // compilation unit node 1192 for (lombok.ast.Node node : nodes) { 1193 if (node instanceof lombok.ast.CompilationUnit) { 1194 return node; 1195 } 1196 } 1197 1198 return null; 1199 } catch (Throwable t) { 1200 AdtPlugin.log(t, "Failed converting ECJ parse tree to Lombok for file %1$s", 1201 context.file.getPath()); 1202 return null; 1203 } 1204 } else { 1205 // Use Lombok for now 1206 Source source = new Source(context.getContents(), context.file.getName()); 1207 List<lombok.ast.Node> nodes = source.getNodes(); 1208 1209 // Don't analyze files containing errors 1210 List<ParseProblem> problems = source.getProblems(); 1211 if (problems != null && problems.size() > 0) { 1212 /* Silently ignore the errors. There are still some bugs in Lombok/Parboiled 1213 * (triggered if you run lint on the AOSP framework directory for example), 1214 * and having these show up as fatal errors when it's really a tool bug 1215 * is bad. To make matters worse, the error messages aren't clear: 1216 * http://code.google.com/p/projectlombok/issues/detail?id=313 1217 for (ParseProblem problem : problems) { 1218 lombok.ast.Position position = problem.getPosition(); 1219 Location location = Location.create(context.file, 1220 context.getContents(), position.getStart(), position.getEnd()); 1221 String message = problem.getMessage(); 1222 context.report( 1223 IssueRegistry.PARSER_ERROR, location, 1224 message, 1225 null); 1226 1227 } 1228 */ 1229 return null; 1230 } 1231 1232 // There could be more than one node when there are errors; pick out the 1233 // compilation unit node 1234 for (lombok.ast.Node node : nodes) { 1235 if (node instanceof lombok.ast.CompilationUnit) { 1236 return node; 1237 } 1238 } 1239 return null; 1240 } 1241 } 1242 1243 @Override 1244 public @NonNull Location getLocation(@NonNull JavaContext context, 1245 @NonNull lombok.ast.Node node) { 1246 lombok.ast.Position position = node.getPosition(); 1247 return Location.create(context.file, context.getContents(), 1248 position.getStart(), position.getEnd()); 1249 } 1250 1251 @Override 1252 public @NonNull Handle createLocationHandle(@NonNull JavaContext context, 1253 @NonNull lombok.ast.Node node) { 1254 return new LocationHandle(context.file, node); 1255 } 1256 1257 @Override 1258 public void dispose(@NonNull JavaContext context, 1259 @NonNull lombok.ast.Node compilationUnit) { 1260 } 1261 1262 @Override 1263 @Nullable 1264 public ResolvedNode resolve(@NonNull JavaContext context, 1265 @NonNull lombok.ast.Node node) { 1266 return null; 1267 } 1268 1269 @Override 1270 @Nullable 1271 public TypeDescriptor getType(@NonNull JavaContext context, 1272 @NonNull lombok.ast.Node node) { 1273 return null; 1274 } 1275 1276 /* Handle for creating positions cheaply and returning full fledged locations later */ 1277 private class LocationHandle implements Handle { 1278 private File mFile; 1279 private lombok.ast.Node mNode; 1280 private Object mClientData; 1281 1282 public LocationHandle(File file, lombok.ast.Node node) { 1283 mFile = file; 1284 mNode = node; 1285 } 1286 1287 @Override 1288 public @NonNull Location resolve() { 1289 lombok.ast.Position pos = mNode.getPosition(); 1290 return Location.create(mFile, null /*contents*/, pos.getStart(), pos.getEnd()); 1291 } 1292 1293 @Override 1294 public void setClientData(@Nullable Object clientData) { 1295 mClientData = clientData; 1296 } 1297 1298 @Override 1299 @Nullable 1300 public Object getClientData() { 1301 return mClientData; 1302 } 1303 } 1304 } 1305 } 1306 1307