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