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