| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.tools.lint; |
| |
| import static com.android.SdkConstants.CURRENT_PLATFORM; |
| import static com.android.SdkConstants.DOT_9PNG; |
| import static com.android.SdkConstants.DOT_PNG; |
| import static com.android.SdkConstants.PLATFORM_LINUX; |
| import static com.android.SdkConstants.UTF_8; |
| import static com.android.tools.lint.detector.api.LintUtils.endsWith; |
| import static java.io.File.separatorChar; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.tools.lint.checks.AccessibilityDetector; |
| import com.android.tools.lint.checks.AlwaysShowActionDetector; |
| import com.android.tools.lint.checks.ApiDetector; |
| import com.android.tools.lint.checks.AppCompatCallDetector; |
| import com.android.tools.lint.checks.ByteOrderMarkDetector; |
| import com.android.tools.lint.checks.CommentDetector; |
| import com.android.tools.lint.checks.DetectMissingPrefix; |
| import com.android.tools.lint.checks.DosLineEndingDetector; |
| import com.android.tools.lint.checks.DuplicateResourceDetector; |
| import com.android.tools.lint.checks.GradleDetector; |
| import com.android.tools.lint.checks.GridLayoutDetector; |
| import com.android.tools.lint.checks.HardcodedValuesDetector; |
| import com.android.tools.lint.checks.IncludeDetector; |
| import com.android.tools.lint.checks.InefficientWeightDetector; |
| import com.android.tools.lint.checks.JavaPerformanceDetector; |
| import com.android.tools.lint.checks.ManifestDetector; |
| import com.android.tools.lint.checks.MissingClassDetector; |
| import com.android.tools.lint.checks.MissingIdDetector; |
| import com.android.tools.lint.checks.NamespaceDetector; |
| import com.android.tools.lint.checks.ObsoleteLayoutParamsDetector; |
| import com.android.tools.lint.checks.PropertyFileDetector; |
| import com.android.tools.lint.checks.PxUsageDetector; |
| import com.android.tools.lint.checks.ScrollViewChildDetector; |
| import com.android.tools.lint.checks.SecurityDetector; |
| import com.android.tools.lint.checks.SharedPrefsDetector; |
| import com.android.tools.lint.checks.SignatureOrSystemDetector; |
| import com.android.tools.lint.checks.SupportAnnotationDetector; |
| import com.android.tools.lint.checks.TextFieldDetector; |
| import com.android.tools.lint.checks.TextViewDetector; |
| import com.android.tools.lint.checks.TitleDetector; |
| import com.android.tools.lint.checks.TranslationDetector; |
| import com.android.tools.lint.checks.TypoDetector; |
| import com.android.tools.lint.checks.TypographyDetector; |
| import com.android.tools.lint.checks.UseCompoundDrawableDetector; |
| import com.android.tools.lint.checks.UselessViewDetector; |
| import com.android.tools.lint.checks.Utf8Detector; |
| import com.android.tools.lint.checks.WrongCallDetector; |
| import com.android.tools.lint.checks.WrongCaseDetector; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.utils.SdkUtils; |
| import com.google.common.annotations.Beta; |
| import com.google.common.collect.Sets; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.Closer; |
| import com.google.common.io.Files; |
| |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.net.MalformedURLException; |
| import java.net.URL; |
| import java.net.URLEncoder; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** A reporter is an output generator for lint warnings |
| * <p> |
| * <b>NOTE: This is not a public or final API; if you rely on this be prepared |
| * to adjust your code for the next tools release.</b> |
| */ |
| @Beta |
| public abstract class Reporter { |
| protected final LintCliClient mClient; |
| protected final File mOutput; |
| protected String mTitle = "Lint Report"; |
| protected boolean mSimpleFormat; |
| protected boolean mBundleResources; |
| protected Map<String, String> mUrlMap; |
| protected File mResources; |
| protected final Map<File, String> mResourceUrl = new HashMap<File, String>(); |
| protected final Map<String, File> mNameToFile = new HashMap<String, File>(); |
| protected boolean mDisplayEmpty = true; |
| |
| /** |
| * Write the given warnings into the report |
| * |
| * @param errorCount the number of errors |
| * @param warningCount the number of warnings |
| * @param issues the issues to be reported |
| * @throws IOException if an error occurs |
| */ |
| public abstract void write(int errorCount, int warningCount, List<Warning> issues) |
| throws IOException; |
| |
| protected Reporter(LintCliClient client, File output) { |
| mClient = client; |
| mOutput = output; |
| } |
| |
| /** |
| * Sets the report title |
| * |
| * @param title the title of the report |
| */ |
| public void setTitle(String title) { |
| mTitle = title; |
| } |
| |
| /** @return the title of the report */ |
| public String getTitle() { |
| return mTitle; |
| } |
| |
| /** |
| * Sets whether the report should bundle up resources along with the HTML report. |
| * This implies a non-simple format (see {@link #setSimpleFormat(boolean)}). |
| * |
| * @param bundleResources if true, copy images into a directory relative to |
| * the report |
| */ |
| public void setBundleResources(boolean bundleResources) { |
| mBundleResources = bundleResources; |
| mSimpleFormat = false; |
| } |
| |
| /** |
| * Sets whether the report should use simple formatting (meaning no JavaScript, |
| * embedded images, etc). |
| * |
| * @param simpleFormat whether the formatting should be simple |
| */ |
| public void setSimpleFormat(boolean simpleFormat) { |
| mSimpleFormat = simpleFormat; |
| } |
| |
| /** |
| * Returns whether the report should use simple formatting (meaning no JavaScript, |
| * embedded images, etc). |
| * |
| * @return whether the report should use simple formatting |
| */ |
| public boolean isSimpleFormat() { |
| return mSimpleFormat; |
| } |
| |
| |
| String getUrl(File file) { |
| if (mBundleResources && !mSimpleFormat) { |
| String url = getRelativeResourceUrl(file); |
| if (url != null) { |
| return url; |
| } |
| } |
| |
| if (mUrlMap != null) { |
| String path = file.getAbsolutePath(); |
| // Perform the comparison using URLs such that we properly escape spaces etc. |
| String pathUrl = encodeUrl(path); |
| for (Map.Entry<String, String> entry : mUrlMap.entrySet()) { |
| String prefix = entry.getKey(); |
| String prefixUrl = encodeUrl(prefix); |
| if (pathUrl.startsWith(prefixUrl)) { |
| String relative = pathUrl.substring(prefixUrl.length()); |
| return entry.getValue() + relative; |
| } |
| } |
| } |
| |
| if (file.isAbsolute()) { |
| String relativePath = getRelativePath(mOutput.getParentFile(), file); |
| if (relativePath != null) { |
| relativePath = relativePath.replace(separatorChar, '/'); |
| return encodeUrl(relativePath); |
| } |
| } |
| |
| try { |
| return SdkUtils.fileToUrlString(file); |
| } catch (MalformedURLException e) { |
| return null; |
| } |
| } |
| |
| /** Encodes the given String as a safe URL substring, escaping spaces etc */ |
| static String encodeUrl(String url) { |
| try { |
| url = url.replace('\\', '/'); |
| return URLEncoder.encode(url, UTF_8).replace("%2F", "/"); //$NON-NLS-1$ |
| } catch (UnsupportedEncodingException e) { |
| // This shouldn't happen for UTF-8 |
| System.err.println("Invalid string " + e.getLocalizedMessage()); |
| return url; |
| } |
| } |
| |
| /** Set mapping of path prefixes to corresponding URLs in the HTML report */ |
| public void setUrlMap(@Nullable Map<String, String> urlMap) { |
| mUrlMap = urlMap; |
| } |
| |
| /** Gets a pointer to the local resource directory, if any */ |
| File getResourceDir() { |
| if (mResources == null && mBundleResources) { |
| mResources = computeResourceDir(); |
| if (mResources == null) { |
| mBundleResources = false; |
| } |
| } |
| |
| return mResources; |
| } |
| |
| /** Finds/creates the local resource directory, if possible */ |
| File computeResourceDir() { |
| String fileName = mOutput.getName(); |
| int dot = fileName.indexOf('.'); |
| if (dot != -1) { |
| fileName = fileName.substring(0, dot); |
| } |
| |
| File resources = new File(mOutput.getParentFile(), fileName + "_files"); //$NON-NLS-1$ |
| if (!resources.exists() && !resources.mkdir()) { |
| resources = null; |
| } |
| |
| return resources; |
| } |
| |
| /** Returns a URL to a local copy of the given file, or null */ |
| protected String getRelativeResourceUrl(File file) { |
| String resource = mResourceUrl.get(file); |
| if (resource != null) { |
| return resource; |
| } |
| |
| String name = file.getName(); |
| if (!endsWith(name, DOT_PNG) || endsWith(name, DOT_9PNG)) { |
| return null; |
| } |
| |
| // Attempt to make local copy |
| File resourceDir = getResourceDir(); |
| if (resourceDir != null) { |
| String base = file.getName(); |
| |
| File path = mNameToFile.get(base); |
| if (path != null && !path.equals(file)) { |
| // That filename already exists and is associated with a different path: |
| // make a new unique version |
| for (int i = 0; i < 100; i++) { |
| base = '_' + base; |
| path = mNameToFile.get(base); |
| if (path == null || path.equals(file)) { |
| break; |
| } |
| } |
| } |
| |
| File target = new File(resourceDir, base); |
| try { |
| Files.copy(file, target); |
| } catch (IOException e) { |
| return null; |
| } |
| return resourceDir.getName() + '/' + encodeUrl(base); |
| } |
| return null; |
| } |
| |
| /** Returns a URL to a local copy of the given resource, or null. There is |
| * no filename conflict resolution. */ |
| protected String addLocalResources(URL url) throws IOException { |
| // Attempt to make local copy |
| File resourceDir = computeResourceDir(); |
| if (resourceDir != null) { |
| String base = url.getFile(); |
| base = base.substring(base.lastIndexOf('/') + 1); |
| mNameToFile.put(base, new File(url.toExternalForm())); |
| |
| File target = new File(resourceDir, base); |
| Closer closer = Closer.create(); |
| try { |
| FileOutputStream output = closer.register(new FileOutputStream(target)); |
| InputStream input = closer.register(url.openStream()); |
| ByteStreams.copy(input, output); |
| } catch (Throwable e) { |
| closer.rethrow(e); |
| } finally { |
| closer.close(); |
| } |
| return resourceDir.getName() + '/' + encodeUrl(base); |
| } |
| return null; |
| } |
| |
| // Based on similar code in com.intellij.openapi.util.io.FileUtilRt |
| @Nullable |
| static String getRelativePath(File base, File file) { |
| if (base == null || file == null) { |
| return null; |
| } |
| if (!base.isDirectory()) { |
| base = base.getParentFile(); |
| if (base == null) { |
| return null; |
| } |
| } |
| if (base.equals(file)) { |
| return "."; |
| } |
| |
| final String filePath = file.getAbsolutePath(); |
| String basePath = base.getAbsolutePath(); |
| |
| // TODO: Make this return null if we go all the way to the root! |
| |
| basePath = !basePath.isEmpty() && basePath.charAt(basePath.length() - 1) == separatorChar |
| ? basePath : basePath + separatorChar; |
| |
| // Whether filesystem is case sensitive. Technically on OSX you could create a |
| // sensitive one, but it's not the default. |
| boolean caseSensitive = CURRENT_PLATFORM == PLATFORM_LINUX; |
| Locale l = Locale.getDefault(); |
| String basePathToCompare = caseSensitive ? basePath : basePath.toLowerCase(l); |
| String filePathToCompare = caseSensitive ? filePath : filePath.toLowerCase(l); |
| if (basePathToCompare.equals(!filePathToCompare.isEmpty() |
| && filePathToCompare.charAt(filePathToCompare.length() - 1) == separatorChar |
| ? filePathToCompare : filePathToCompare + separatorChar)) { |
| return "."; |
| } |
| int len = 0; |
| int lastSeparatorIndex = 0; |
| // bug in inspection; see http://youtrack.jetbrains.com/issue/IDEA-118971 |
| //noinspection ConstantConditions |
| while (len < filePath.length() && len < basePath.length() |
| && filePathToCompare.charAt(len) == basePathToCompare.charAt(len)) { |
| if (basePath.charAt(len) == separatorChar) { |
| lastSeparatorIndex = len; |
| } |
| len++; |
| } |
| if (len == 0) { |
| return null; |
| } |
| |
| StringBuilder relativePath = new StringBuilder(); |
| for (int i = len; i < basePath.length(); i++) { |
| if (basePath.charAt(i) == separatorChar) { |
| relativePath.append(".."); |
| relativePath.append(separatorChar); |
| } |
| } |
| relativePath.append(filePath.substring(lastSeparatorIndex + 1)); |
| return relativePath.toString(); |
| } |
| |
| /** |
| * Returns whether this report should display info (such as a path to the report) if |
| * no issues were found |
| */ |
| public boolean isDisplayEmpty() { |
| return mDisplayEmpty; |
| } |
| |
| /** |
| * Sets whether this report should display info (such as a path to the report) if |
| * no issues were found |
| */ |
| public void setDisplayEmpty(boolean displayEmpty) { |
| mDisplayEmpty = displayEmpty; |
| } |
| |
| private static Set<Issue> sAdtFixes; |
| private static Set<Issue> sStudioFixes; |
| |
| /** Tools known to have quickfixes for lint */ |
| enum QuickfixHandler { |
| /** Android Studio or IntelliJ */ |
| STUDIO, |
| /** Eclipse */ |
| ADT; |
| |
| public boolean hasAutoFix(Issue issue) { |
| return Reporter.hasAutoFix(this, issue); |
| } |
| } |
| |
| /** |
| * Returns true if the given issue has an automatic IDE fix. |
| * |
| * @param tool the name of the tool to be checked |
| * @param issue the issue to be checked |
| * @return true if the given tool is known to have an automatic fix for the |
| * given issue |
| */ |
| public static boolean hasAutoFix(@NonNull QuickfixHandler tool, Issue issue) { |
| if (tool == QuickfixHandler.ADT) { |
| if (sAdtFixes == null) { |
| sAdtFixes = Sets.newHashSet( |
| InefficientWeightDetector.INEFFICIENT_WEIGHT, |
| AccessibilityDetector.ISSUE, |
| InefficientWeightDetector.BASELINE_WEIGHTS, |
| HardcodedValuesDetector.ISSUE, |
| UselessViewDetector.USELESS_LEAF, |
| UselessViewDetector.USELESS_PARENT, |
| PxUsageDetector.PX_ISSUE, |
| TextFieldDetector.ISSUE, |
| SecurityDetector.EXPORTED_SERVICE, |
| DetectMissingPrefix.MISSING_NAMESPACE, |
| ScrollViewChildDetector.ISSUE, |
| ObsoleteLayoutParamsDetector.ISSUE, |
| TypographyDetector.DASHES, |
| TypographyDetector.ELLIPSIS, |
| TypographyDetector.FRACTIONS, |
| TypographyDetector.OTHER, |
| TypographyDetector.QUOTES, |
| UseCompoundDrawableDetector.ISSUE, |
| ApiDetector.UNSUPPORTED, |
| ApiDetector.INLINED, |
| TypoDetector.ISSUE, |
| ManifestDetector.ALLOW_BACKUP, |
| MissingIdDetector.ISSUE, |
| TranslationDetector.MISSING, |
| DosLineEndingDetector.ISSUE |
| ); |
| } |
| return sAdtFixes.contains(issue); |
| } else if (tool == QuickfixHandler.STUDIO) { |
| // List generated by AndroidLintInspectionToolProviderTest in tools/adt/idea; |
| // set LIST_ISSUES_WITH_QUICK_FIXES to true |
| if (sStudioFixes == null) { |
| sStudioFixes = Sets.newHashSet( |
| AccessibilityDetector.ISSUE, |
| AlwaysShowActionDetector.ISSUE, |
| ApiDetector.INLINED, |
| ApiDetector.UNSUPPORTED, |
| AppCompatCallDetector.ISSUE, |
| ByteOrderMarkDetector.BOM, |
| CommentDetector.STOP_SHIP, |
| DetectMissingPrefix.MISSING_NAMESPACE, |
| DuplicateResourceDetector.TYPE_MISMATCH, |
| GradleDetector.COMPATIBILITY, |
| GradleDetector.DEPENDENCY, |
| GradleDetector.DEPRECATED, |
| GradleDetector.PLUS, |
| GradleDetector.REMOTE_VERSION, |
| GradleDetector.STRING_INTEGER, |
| GridLayoutDetector.ISSUE, |
| IncludeDetector.ISSUE, |
| InefficientWeightDetector.BASELINE_WEIGHTS, |
| InefficientWeightDetector.INEFFICIENT_WEIGHT, |
| InefficientWeightDetector.ORIENTATION, |
| JavaPerformanceDetector.USE_VALUE_OF, |
| ManifestDetector.ALLOW_BACKUP, |
| ManifestDetector.APPLICATION_ICON, |
| ManifestDetector.MIPMAP, |
| ManifestDetector.MOCK_LOCATION, |
| ManifestDetector.TARGET_NEWER, |
| MissingClassDetector.INNERCLASS, |
| MissingIdDetector.ISSUE, |
| NamespaceDetector.RES_AUTO, |
| ObsoleteLayoutParamsDetector.ISSUE, |
| PropertyFileDetector.ESCAPE, |
| PropertyFileDetector.HTTP, |
| PxUsageDetector.DP_ISSUE, |
| PxUsageDetector.PX_ISSUE, |
| ScrollViewChildDetector.ISSUE, |
| SecurityDetector.EXPORTED_SERVICE, |
| SharedPrefsDetector.ISSUE, |
| SignatureOrSystemDetector.ISSUE, |
| SupportAnnotationDetector.CHECK_PERMISSION, |
| SupportAnnotationDetector.CHECK_RESULT, |
| TextFieldDetector.ISSUE, |
| TextViewDetector.SELECTABLE, |
| TitleDetector.ISSUE, |
| TypoDetector.ISSUE, |
| TypographyDetector.DASHES, |
| TypographyDetector.ELLIPSIS, |
| TypographyDetector.FRACTIONS, |
| TypographyDetector.OTHER, |
| TypographyDetector.QUOTES, |
| UselessViewDetector.USELESS_LEAF, |
| Utf8Detector.ISSUE, |
| WrongCallDetector.ISSUE, |
| WrongCaseDetector.WRONG_CASE |
| ); |
| } |
| return sStudioFixes.contains(issue); |
| } |
| |
| return false; |
| } |
| } |