1 /* 2 * Copyright (C) 2012 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_CLASS; 19 import static com.android.SdkConstants.DOT_JAVA; 20 import static com.android.SdkConstants.EXT_JAVA; 21 22 import com.android.annotations.NonNull; 23 import com.android.annotations.Nullable; 24 import com.android.ide.eclipse.adt.AdtPlugin; 25 import com.android.ide.eclipse.adt.AdtUtils; 26 import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; 27 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor; 28 import com.android.ide.eclipse.adt.internal.resources.manager.GlobalProjectMonitor.IFileListener; 29 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 30 31 import org.eclipse.core.resources.IFile; 32 import org.eclipse.core.resources.IMarkerDelta; 33 import org.eclipse.core.resources.IResource; 34 import org.eclipse.core.resources.IResourceDelta; 35 import org.eclipse.swt.widgets.Display; 36 37 import java.util.ArrayList; 38 import java.util.Collections; 39 import java.util.List; 40 41 /** 42 * Delta processor for Java files, which runs single-file lints if it finds that 43 * the currently active file has been updated. 44 */ 45 public class LintDeltaProcessor implements Runnable { 46 private List<IResource> mFiles; 47 private IFile mActiveFile; 48 49 private LintDeltaProcessor() { 50 // Get the active editor file, if any 51 Display display = AdtPlugin.getDisplay(); 52 if (display == null || display.isDisposed()) { 53 return; 54 } 55 if (display.getThread() != Thread.currentThread()) { 56 display.syncExec(this); 57 } else { 58 run(); 59 } 60 } 61 62 /** 63 * Creates a new {@link LintDeltaProcessor} 64 * 65 * @return a visitor 66 */ 67 @NonNull 68 public static LintDeltaProcessor create() { 69 return new LintDeltaProcessor(); 70 } 71 72 /** 73 * Process the given delta: update lint on any Java source and class files found. 74 * 75 * @param delta the delta describing recently changed files 76 */ 77 public void process(@NonNull IResourceDelta delta) { 78 if (mActiveFile == null || !mActiveFile.getName().endsWith(DOT_JAVA)) { 79 return; 80 } 81 82 mFiles = new ArrayList<IResource>(); 83 gatherFiles(delta); 84 85 if (!mFiles.isEmpty()) { 86 EclipseLintRunner.startLint(mFiles, mActiveFile, null, 87 false /*fatalOnly*/, false /*show*/); 88 } 89 } 90 91 /** 92 * Process edits in the given file: update lint on the Java source provided 93 * it's the active file. 94 * 95 * @param file the file that was changed 96 */ 97 public void process(@NonNull IFile file) { 98 if (mActiveFile == null || !mActiveFile.getName().endsWith(DOT_JAVA)) { 99 return; 100 } 101 102 if (file.equals(mActiveFile)) { 103 mFiles = Collections.<IResource>singletonList(file); 104 EclipseLintRunner.startLint(mFiles, mActiveFile, null, 105 false /*fatalOnly*/, false /*show*/); 106 } 107 } 108 109 /** 110 * Collect .java and .class files to be run in lint. Only collects files 111 * that match the active editor. 112 */ 113 private void gatherFiles(@NonNull IResourceDelta delta) { 114 IResource resource = delta.getResource(); 115 String name = resource.getName(); 116 if (name.endsWith(DOT_JAVA)) { 117 if (resource.equals(mActiveFile)) { 118 mFiles.add(resource); 119 } 120 } else if (name.endsWith(DOT_CLASS)) { 121 // Make sure this class corresponds to the .java file, meaning it has 122 // the same basename, or that it is an inner class of a class that 123 // matches the same basename. (We could potentially make sure the package 124 // names match too, but it's unlikely that the class names match without a 125 // package match, and there's no harm in including some extra classes here, 126 // since lint will resolve full paths and the resource markers won't go 127 // to the wrong place, we simply end up analyzing some extra files.) 128 String className = mActiveFile.getName(); 129 if (name.regionMatches(0, className, 0, className.length() - DOT_JAVA.length())) { 130 if (name.length() == className.length() - DOT_JAVA.length() + DOT_CLASS.length() 131 || name.charAt(className.length() - DOT_JAVA.length()) == '$') { 132 mFiles.add(resource); 133 } 134 } 135 } else { 136 IResourceDelta[] children = delta.getAffectedChildren(); 137 if (children != null && children.length > 0) { 138 for (IResourceDelta d : children) { 139 gatherFiles(d); 140 } 141 } 142 } 143 } 144 145 @Override 146 public void run() { 147 // Get the active file: this must be run on the GUI thread 148 mActiveFile = AdtUtils.getActiveFile(); 149 } 150 151 /** 152 * Start listening to the resource monitor 153 * 154 * @param resourceMonitor the resource monitor 155 */ 156 public static void startListening(@NonNull GlobalProjectMonitor resourceMonitor) { 157 // Add a file listener which finds out when files have changed. This is listening 158 // specifically for saves of Java files, in order to run incremental lint on them. 159 // Note that the {@link PostCompilerBuilder} already handles incremental lint files 160 // on Java files - and runs it for both the .java and .class files. 161 // 162 // However, if Project > Build Automatically is turned off, then the PostCompilerBuilder 163 // isn't run after a save. THAT's what the below is for: it will run and *only* 164 // run lint incrementally if build automatically is off. 165 assert sListener == null; // Should only be called once on plugin activation 166 sListener = new IFileListener() { 167 @Override 168 public void fileChanged(@NonNull IFile file, 169 @NonNull IMarkerDelta[] markerDeltas, 170 int kind, @Nullable String extension, int flags, boolean isAndroidProject) { 171 if (!isAndroidProject || flags == IResourceDelta.MARKERS) { 172 // If not an Android project or ONLY the markers changed. 173 // Ignore these since they happen 174 // when we add markers for lint errors found in the current file, 175 // which would cause us to repeatedly enter this method over and over 176 // again. 177 return; 178 } 179 if (EXT_JAVA.equals(extension) 180 && !ResourceManager.isAutoBuilding() 181 && AdtPrefs.getPrefs().isLintOnSave()) { 182 LintDeltaProcessor.create().process(file); 183 } 184 } 185 }; 186 resourceMonitor.addFileListener(sListener, IResourceDelta.ADDED | IResourceDelta.CHANGED); 187 } 188 189 /** 190 * Stop listening to the resource monitor 191 * 192 * @param resourceMonitor the resource monitor 193 */ 194 public static void stopListening(@NonNull GlobalProjectMonitor resourceMonitor) { 195 assert sListener != null; 196 resourceMonitor.removeFileListener(sListener); 197 sListener = null; 198 } 199 200 private static IFileListener sListener; 201 } 202