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