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