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