| /* |
| * Copyright (C) 2014 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.checks; |
| |
| import static com.android.SdkConstants.FD_BUILD_TOOLS; |
| import static com.android.SdkConstants.GRADLE_PLUGIN_MINIMUM_VERSION; |
| import static com.android.SdkConstants.GRADLE_PLUGIN_RECOMMENDED_VERSION; |
| import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_HIGHER; |
| import static com.android.tools.lint.checks.ManifestDetector.TARGET_NEWER; |
| import static com.android.tools.lint.detector.api.LintUtils.findSubstring; |
| import static com.google.common.base.Charsets.UTF_8; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.builder.model.AndroidLibrary; |
| import com.android.builder.model.Dependencies; |
| import com.android.builder.model.MavenCoordinates; |
| import com.android.builder.model.Variant; |
| import com.android.ide.common.repository.GradleCoordinate; |
| import com.android.ide.common.repository.GradleCoordinate.RevisionComponent; |
| import com.android.ide.common.repository.SdkMavenRepository; |
| import com.android.sdklib.repository.PreciseRevision; |
| import com.android.tools.lint.client.api.LintClient; |
| import com.android.tools.lint.detector.api.Category; |
| import com.android.tools.lint.detector.api.Context; |
| import com.android.tools.lint.detector.api.Detector; |
| import com.android.tools.lint.detector.api.Implementation; |
| import com.android.tools.lint.detector.api.Issue; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Location; |
| import com.android.tools.lint.detector.api.Scope; |
| import com.android.tools.lint.detector.api.Severity; |
| import com.android.tools.lint.detector.api.Speed; |
| import com.android.tools.lint.detector.api.TextFormat; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.io.InputStreamReader; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URL; |
| import java.net.URLConnection; |
| import java.net.URLEncoder; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| |
| /** |
| * Checks Gradle files for potential errors |
| */ |
| public class GradleDetector extends Detector implements Detector.GradleScanner { |
| |
| private static final Implementation IMPLEMENTATION = new Implementation( |
| GradleDetector.class, |
| Scope.GRADLE_SCOPE); |
| |
| /** Obsolete dependencies */ |
| public static final Issue DEPENDENCY = Issue.create( |
| "GradleDependency", //$NON-NLS-1$ |
| "Obsolete Gradle Dependency", |
| "This detector looks for usages of libraries where the version you are using " + |
| "is not the current stable release. Using older versions is fine, and there are " + |
| "cases where you deliberately want to stick with an older version. However, " + |
| "you may simply not be aware that a more recent version is available, and that is " + |
| "what this lint check helps find.", |
| Category.CORRECTNESS, |
| 4, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Deprecated Gradle constructs */ |
| public static final Issue DEPRECATED = Issue.create( |
| "GradleDeprecated", //$NON-NLS-1$ |
| "Deprecated Gradle Construct", |
| "This detector looks for deprecated Gradle constructs which currently work but " + |
| "will likely stop working in a future update.", |
| Category.CORRECTNESS, |
| 6, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Incompatible Android Gradle plugin */ |
| public static final Issue GRADLE_PLUGIN_COMPATIBILITY = Issue.create( |
| "AndroidGradlePluginVersion", //$NON-NLS-1$ |
| "Incompatible Android Gradle Plugin", |
| "Not all versions of the Android Gradle plugin are compatible with all versions " + |
| "of the SDK. If you update your tools, or if you are trying to open a project that " + |
| "was built with an old version of the tools, you may need to update your plugin " + |
| "version number.", |
| Category.CORRECTNESS, |
| 8, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** Invalid or dangerous paths */ |
| public static final Issue PATH = Issue.create( |
| "GradlePath", //$NON-NLS-1$ |
| "Gradle Path Issues", |
| "Gradle build scripts are meant to be cross platform, so file paths use " + |
| "Unix-style path separators (a forward slash) rather than Windows path separators " + |
| "(a backslash). Similarly, to keep projects portable and repeatable, avoid " + |
| "using absolute paths on the system; keep files within the project instead. To " + |
| "share code between projects, consider creating an android-library and an AAR " + |
| "dependency", |
| Category.CORRECTNESS, |
| 4, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Constructs the IDE support struggles with */ |
| public static final Issue IDE_SUPPORT = Issue.create( |
| "GradleIdeError", //$NON-NLS-1$ |
| "Gradle IDE Support Issues", |
| "Gradle is highly flexible, and there are things you can do in Gradle files which " + |
| "can make it hard or impossible for IDEs to properly handle the project. This lint " + |
| "check looks for constructs that potentially break IDE support.", |
| Category.CORRECTNESS, |
| 4, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** Using + in versions */ |
| public static final Issue PLUS = Issue.create( |
| "GradleDynamicVersion", //$NON-NLS-1$ |
| "Gradle Dynamic Version", |
| "Using `+` in dependencies lets you automatically pick up the latest available " + |
| "version rather than a specific, named version. However, this is not recommended; " + |
| "your builds are not repeatable; you may have tested with a slightly different " + |
| "version than what the build server used. (Using a dynamic version as the major " + |
| "version number is more problematic than using it in the minor version position.)", |
| Category.CORRECTNESS, |
| 4, |
| Severity.WARNING, |
| IMPLEMENTATION); |
| |
| /** Accidentally calling a getter instead of your own methods */ |
| public static final Issue GRADLE_GETTER = Issue.create( |
| "GradleGetter", //$NON-NLS-1$ |
| "Gradle Implicit Getter Call", |
| "Gradle will let you replace specific constants in your build scripts with method " + |
| "calls, so you can for example dynamically compute a version string based on your " + |
| "current version control revision number, rather than hardcoding a number.\n" + |
| "\n" + |
| "When computing a version name, it's tempting to for example call the method to do " + |
| "that `getVersionName`. However, when you put that method call inside the " + |
| "`defaultConfig` block, you will actually be calling the Groovy getter for the " + |
| "`versionName` property instead. Therefore, you need to name your method something " + |
| "which does not conflict with the existing implicit getters. Consider using " + |
| "`compute` as a prefix instead of `get`.", |
| Category.CORRECTNESS, |
| 6, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** Using incompatible versions */ |
| public static final Issue COMPATIBILITY = Issue.create( |
| "GradleCompatible", //$NON-NLS-1$ |
| "Incompatible Gradle Versions", |
| |
| "There are some combinations of libraries, or tools and libraries, that are " + |
| "incompatible, or can lead to bugs. One such incompatibility is compiling with " + |
| "a version of the Android support libraries that is not the latest version (or in " + |
| "particular, a version lower than your `targetSdkVersion`.)", |
| |
| Category.CORRECTNESS, |
| 8, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** Using a string where an integer is expected */ |
| public static final Issue STRING_INTEGER = Issue.create( |
| "StringShouldBeInt", //$NON-NLS-1$ |
| "String should be int", |
| |
| "The properties `compileSdkVersion`, `minSdkVersion` and `targetSdkVersion` are " + |
| "usually numbers, but can be strings when you are using an add-on (in the case " + |
| "of `compileSdkVersion`) or a preview platform (for the other two properties).\n" + |
| "\n" + |
| "However, you can not use a number as a string (e.g. \"19\" instead of 19); that " + |
| "will result in a platform not found error message at build/sync time.", |
| |
| Category.CORRECTNESS, |
| 8, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** A newer version is available on a remote server */ |
| public static final Issue REMOTE_VERSION = Issue.create( |
| "NewerVersionAvailable", //$NON-NLS-1$ |
| "Newer Library Versions Available", |
| "This detector checks with a central repository to see if there are newer versions " + |
| "available for the dependencies used by this project. " + |
| "This is similar to the `GradleDependency` check, which checks for newer versions " + |
| "available in the Android SDK tools and libraries, but this works with any " + |
| "MavenCentral dependency, and connects to the library every time, which makes " + |
| "it more flexible but also *much* slower.", |
| Category.CORRECTNESS, |
| 4, |
| Severity.WARNING, |
| IMPLEMENTATION).setEnabledByDefault(false); |
| |
| /** Accidentally using octal numbers */ |
| public static final Issue ACCIDENTAL_OCTAL = Issue.create( |
| "AccidentalOctal", //$NON-NLS-1$ |
| "Accidental Octal", |
| |
| "In Groovy, an integer literal that starts with a leading 0 will be interpreted " + |
| "as an octal number. That is usually (always?) an accident and can lead to " + |
| "subtle bugs, for example when used in the `versionCode` of an app.", |
| |
| Category.CORRECTNESS, |
| 2, |
| Severity.ERROR, |
| IMPLEMENTATION); |
| |
| /** The Gradle plugin ID for Android applications */ |
| public static final String APP_PLUGIN_ID = "com.android.application"; |
| /** The Gradle plugin ID for Android libraries */ |
| public static final String LIB_PLUGIN_ID = "com.android.library"; |
| |
| /** Previous plugin id for applications */ |
| public static final String OLD_APP_PLUGIN_ID = "android"; |
| /** Previous plugin id for libraries */ |
| public static final String OLD_LIB_PLUGIN_ID = "android-library"; |
| |
| private int mMinSdkVersion; |
| private int mCompileSdkVersion; |
| private int mTargetSdkVersion; |
| |
| @Override |
| public boolean appliesTo(@NonNull Context context, @NonNull File file) { |
| return true; |
| } |
| |
| @Override |
| @NonNull |
| public Speed getSpeed(@SuppressWarnings("UnusedParameters") @NonNull Issue issue) { |
| return issue == REMOTE_VERSION ? Speed.REALLY_SLOW : Speed.NORMAL; |
| } |
| |
| // ---- Implements Detector.GradleScanner ---- |
| |
| @Override |
| public void visitBuildScript(@NonNull Context context, Map<String, Object> sharedData) { |
| } |
| |
| @SuppressWarnings("UnusedDeclaration") |
| protected static boolean isInterestingBlock( |
| @NonNull String parent, |
| @Nullable String parentParent) { |
| return parent.equals("defaultConfig") |
| || parent.equals("android") |
| || parent.equals("dependencies") |
| || parent.equals("repositories") |
| || parentParent != null && parentParent.equals("buildTypes"); |
| } |
| |
| protected static boolean isInterestingStatement( |
| @NonNull String statement, |
| @Nullable String parent) { |
| return parent == null && statement.equals("apply"); |
| } |
| |
| @SuppressWarnings("UnusedDeclaration") |
| protected static boolean isInterestingProperty( |
| @NonNull String property, |
| @SuppressWarnings("UnusedParameters") |
| @NonNull String parent, |
| @Nullable String parentParent) { |
| return property.equals("targetSdkVersion") |
| || property.equals("buildToolsVersion") |
| || property.equals("versionName") |
| || property.equals("versionCode") |
| || property.equals("compileSdkVersion") |
| || property.equals("minSdkVersion") |
| || property.equals("applicationIdSuffix") |
| || property.equals("packageName") |
| || property.equals("packageNameSuffix") |
| || parent.equals("dependencies"); |
| } |
| |
| protected void checkOctal( |
| @NonNull Context context, |
| @NonNull String value, |
| @NonNull Object cookie) { |
| if (value.length() >= 2 |
| && value.charAt(0) == '0' |
| && (value.length() > 2 || value.charAt(1) >= '8' |
| && isInteger(value)) |
| && context.isEnabled(ACCIDENTAL_OCTAL)) { |
| String message = "The leading 0 turns this number into octal which is probably " |
| + "not what was intended"; |
| try { |
| long numericValue = Long.decode(value); |
| message += " (interpreted as " + numericValue + ")"; |
| } catch (NumberFormatException nufe) { |
| message += " (and it is not a valid octal number)"; |
| } |
| report(context, cookie, ACCIDENTAL_OCTAL, message); |
| } |
| } |
| |
| /** Called with for example "android", "defaultConfig", "minSdkVersion", "7" */ |
| @SuppressWarnings("UnusedDeclaration") |
| protected void checkDslPropertyAssignment( |
| @NonNull Context context, |
| @NonNull String property, |
| @NonNull String value, |
| @NonNull String parent, |
| @Nullable String parentParent, |
| @NonNull Object valueCookie, |
| @NonNull Object statementCookie) { |
| if (parent.equals("defaultConfig")) { |
| if (property.equals("targetSdkVersion")) { |
| int version = getIntLiteralValue(value, -1); |
| if (version > 0 && version < context.getClient().getHighestKnownApiLevel()) { |
| String message = |
| "Not targeting the latest versions of Android; compatibility " + |
| "modes apply. Consider testing and updating this version. " + |
| "Consult the android.os.Build.VERSION_CODES javadoc for details."; |
| report(context, valueCookie, TARGET_NEWER, message); |
| } |
| if (version > 0) { |
| mTargetSdkVersion = version; |
| checkTargetCompatibility(context, valueCookie); |
| } else { |
| checkIntegerAsString(context, value, valueCookie); |
| } |
| } else if (property.equals("minSdkVersion")) { |
| int version = getIntLiteralValue(value, -1); |
| if (version > 0) { |
| mMinSdkVersion = version; |
| } else { |
| checkIntegerAsString(context, value, valueCookie); |
| } |
| } |
| |
| if (value.startsWith("0")) { |
| checkOctal(context, value, valueCookie); |
| } |
| |
| if (property.equals("versionName") || property.equals("versionCode") && |
| !isInteger(value) || !isStringLiteral(value)) { |
| // Method call -- make sure it does not match one of the getters in the |
| // configuration! |
| if ((value.equals("getVersionCode") || |
| value.equals("getVersionName"))) { |
| String message = "Bad method name: pick a unique method name which does not " |
| + "conflict with the implicit getters for the defaultConfig " |
| + "properties. For example, try using the prefix compute- " |
| + "instead of get-."; |
| report(context, valueCookie, GRADLE_GETTER, message); |
| } |
| } else if (property.equals("packageName")) { |
| if (isModelOlderThan011(context)) { |
| return; |
| } |
| String message = "Deprecated: Replace 'packageName' with 'applicationId'"; |
| report(context, getPropertyKeyCookie(valueCookie), DEPRECATED, message); |
| } |
| } else if (property.equals("compileSdkVersion") && parent.equals("android")) { |
| int version = getIntLiteralValue(value, -1); |
| if (version > 0) { |
| mCompileSdkVersion = version; |
| checkTargetCompatibility(context, valueCookie); |
| } else { |
| checkIntegerAsString(context, value, valueCookie); |
| } |
| } else if (property.equals("buildToolsVersion") && parent.equals("android")) { |
| String versionString = getStringLiteralValue(value); |
| if (versionString != null) { |
| PreciseRevision version = parseRevisionSilently(versionString); |
| if (version != null) { |
| PreciseRevision recommended = getLatestBuildTools(context.getClient(), |
| version.getMajor()); |
| if (recommended != null && version.compareTo(recommended) < 0) { |
| // Keep in sync with {@link #getOldValue} and {@link #getNewValue} |
| String message = "Old buildToolsVersion " + version + |
| "; recommended version is " + recommended + " or later"; |
| report(context, valueCookie, DEPENDENCY, message); |
| } |
| } |
| } |
| } else if (parent.equals("dependencies")) { |
| if (value.startsWith("files('") && value.endsWith("')")) { |
| String path = value.substring("files('".length(), value.length() - 2); |
| if (path.contains("\\\\")) { |
| String message = "Do not use Windows file separators in .gradle files; " |
| + "use / instead"; |
| report(context, valueCookie, PATH, message); |
| |
| } else if (new File(path.replace('/', File.separatorChar)).isAbsolute()) { |
| String message = "Avoid using absolute paths in .gradle files"; |
| report(context, valueCookie, PATH, message); |
| } |
| } else { |
| String dependency = getStringLiteralValue(value); |
| if (dependency == null) { |
| dependency = getNamedDependency(value); |
| } |
| // If the dependency is a GString (i.e. it uses Groovy variable substitution, |
| // with a $variable_name syntax) then don't try to parse it. |
| if (dependency != null) { |
| GradleCoordinate gc = GradleCoordinate.parseCoordinateString(dependency); |
| if (gc != null && dependency.contains("$")) { |
| gc = resolveCoordinate(context, gc); |
| } |
| if (gc != null) { |
| if (gc.acceptsGreaterRevisions()) { |
| String message = "Avoid using + in version numbers; can lead " |
| + "to unpredictable and unrepeatable builds (" + dependency + ")"; |
| report(context, valueCookie, PLUS, message); |
| } |
| if (!dependency.startsWith(SdkConstants.GRADLE_PLUGIN_NAME) || |
| !checkGradlePluginDependency(context, gc, valueCookie)) { |
| checkDependency(context, gc, valueCookie); |
| } |
| } |
| } |
| } |
| } else if (property.equals("packageNameSuffix")) { |
| if (isModelOlderThan011(context)) { |
| return; |
| } |
| String message = "Deprecated: Replace 'packageNameSuffix' with 'applicationIdSuffix'"; |
| report(context, getPropertyKeyCookie(valueCookie), DEPRECATED, message); |
| } else if (property.equals("applicationIdSuffix")) { |
| String suffix = getStringLiteralValue(value); |
| if (suffix != null && !suffix.startsWith(".")) { |
| String message = "Package suffix should probably start with a \".\""; |
| report(context, valueCookie, PATH, message); |
| } |
| } |
| } |
| |
| @Nullable |
| private static GradleCoordinate resolveCoordinate(@NonNull Context context, |
| @NonNull GradleCoordinate gc) { |
| assert gc.getFullRevision().contains("$") : gc.getFullRevision(); |
| Variant variant = context.getProject().getCurrentVariant(); |
| if (variant != null) { |
| Dependencies dependencies = variant.getMainArtifact().getDependencies(); |
| for (AndroidLibrary library : dependencies.getLibraries()) { |
| MavenCoordinates mc = library.getResolvedCoordinates(); |
| if (mc != null |
| && mc.getGroupId().equals(gc.getGroupId()) |
| && mc.getArtifactId().equals(gc.getArtifactId())) { |
| List<RevisionComponent> revisions = |
| GradleCoordinate.parseRevisionNumber(mc.getVersion()); |
| if (!revisions.isEmpty()) { |
| return new GradleCoordinate(mc.getGroupId(), mc.getArtifactId(), |
| revisions, null); |
| } |
| break; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| // Convert a long-hand dependency, like |
| // group: 'com.android.support', name: 'support-v4', version: '21.0.+' |
| // into an equivalent short-hand dependency, like |
| // com.android.support:support-v4:21.0.+ |
| @VisibleForTesting |
| @Nullable |
| static String getNamedDependency(@NonNull String expression) { |
| //if (value.startsWith("group: 'com.android.support', name: 'support-v4', version: '21.0.+'")) |
| if (expression.indexOf(',') != -1 && expression.contains("version:")) { |
| String artifact = null; |
| String group = null; |
| String version = null; |
| Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults(); |
| for (String property : splitter.split(expression)) { |
| int colon = property.indexOf(':'); |
| if (colon == -1) { |
| return null; |
| } |
| char quote = '\''; |
| int valueStart = property.indexOf(quote, colon + 1); |
| if (valueStart == -1) { |
| quote = '"'; |
| valueStart = property.indexOf(quote, colon + 1); |
| } |
| if (valueStart == -1) { |
| // For example, "transitive: false" |
| continue; |
| } |
| valueStart++; |
| int valueEnd = property.indexOf(quote, valueStart); |
| if (valueEnd == -1) { |
| return null; |
| } |
| String value = property.substring(valueStart, valueEnd); |
| if (property.startsWith("group:")) { |
| group = value; |
| } else if (property.startsWith("name:")) { |
| artifact = value; |
| } else if (property.startsWith("version:")) { |
| version = value; |
| } |
| } |
| |
| if (artifact != null && group != null && version != null) { |
| return group + ':' + artifact + ':' + version; |
| } |
| } |
| |
| return null; |
| } |
| |
| private void checkIntegerAsString(Context context, String value, Object valueCookie) { |
| // When done developing with a preview platform you might be tempted to switch from |
| // compileSdkVersion 'android-G' |
| // to |
| // compileSdkVersion '19' |
| // but that won't work; it needs to be |
| // compileSdkVersion 19 |
| String string = getStringLiteralValue(value); |
| if (isNumberString(string)) { |
| String quote = Character.toString(value.charAt(0)); |
| String message = String.format("Use an integer rather than a string here " |
| + "(replace %1$s%2$s%1$s with just %2$s)", quote, string); |
| report(context, valueCookie, STRING_INTEGER, message); |
| } |
| } |
| |
| /** |
| * Given an error message produced by this lint detector for the given issue type, |
| * returns the old value to be replaced in the source code. |
| * <p> |
| * Intended for IDE quickfix implementations. |
| * |
| * @param issue the corresponding issue |
| * @param errorMessage the error message associated with the error |
| * @param format the format of the error message |
| * @return the corresponding old value, or null if not recognized |
| */ |
| @Nullable |
| public static String getOldValue(@NonNull Issue issue, @NonNull String errorMessage, |
| @NonNull TextFormat format) { |
| errorMessage = format.toText(errorMessage); |
| |
| // Consider extracting all the error strings as constants and handling this |
| // using the LintUtils#getFormattedParameters() method to pull back out the information |
| if (issue == DEPENDENCY) { |
| // "A newer version of com.google.guava:guava than 11.0.2 is available: 17.0.0" |
| if (errorMessage.startsWith("A newer ")) { |
| return findSubstring(errorMessage, " than ", " "); |
| } |
| if (errorMessage.startsWith("Old buildToolsVersion ")) { |
| return findSubstring(errorMessage, "Old buildToolsVersion ", ";"); |
| } |
| // "The targetSdkVersion (20) should not be higher than the compileSdkVersion (19)" |
| return findSubstring(errorMessage, "targetSdkVersion (", ")"); |
| } else if (issue == STRING_INTEGER) { |
| return findSubstring(errorMessage, "replace ", " with "); |
| } else if (issue == DEPRECATED) { |
| if (errorMessage.contains(GradleDetector.APP_PLUGIN_ID) && |
| errorMessage.contains(GradleDetector.OLD_APP_PLUGIN_ID)) { |
| return GradleDetector.OLD_APP_PLUGIN_ID; |
| } else if (errorMessage.contains(GradleDetector.LIB_PLUGIN_ID) && |
| errorMessage.contains(GradleDetector.OLD_LIB_PLUGIN_ID)) { |
| return GradleDetector.OLD_LIB_PLUGIN_ID; |
| } |
| // "Deprecated: Replace 'packageNameSuffix' with 'applicationIdSuffix'" |
| return findSubstring(errorMessage, "Replace '", "'"); |
| } else if (issue == PLUS) { |
| return findSubstring(errorMessage, "(", ")"); |
| } else if (issue == COMPATIBILITY) { |
| if (errorMessage.startsWith("Version 5.2.08")) { |
| return "5.2.08"; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Given an error message produced by this lint detector for the given issue type, |
| * returns the new value to be put into the source code. |
| * <p> |
| * Intended for IDE quickfix implementations. |
| * |
| * @param issue the corresponding issue |
| * @param errorMessage the error message associated with the error |
| * @param format the format of the error message |
| * @return the corresponding new value, or null if not recognized |
| */ |
| @Nullable |
| public static String getNewValue(@NonNull Issue issue, @NonNull String errorMessage, |
| @NonNull TextFormat format) { |
| errorMessage = format.toText(errorMessage); |
| |
| if (issue == DEPENDENCY) { |
| // "A newer version of com.google.guava:guava than 11.0.2 is available: 17.0.0" |
| if (errorMessage.startsWith("A newer ")) { |
| return findSubstring(errorMessage, " is available: ", null); |
| } |
| if (errorMessage.startsWith("Old buildToolsVersion ")) { |
| return findSubstring(errorMessage, " version is ", " "); |
| } |
| // "The targetSdkVersion (20) should not be higher than the compileSdkVersion (19)" |
| return findSubstring(errorMessage, "compileSdkVersion (", ")"); |
| } else if (issue == STRING_INTEGER) { |
| return findSubstring(errorMessage, " just ", ")"); |
| } else if (issue == DEPRECATED) { |
| if (errorMessage.contains(GradleDetector.APP_PLUGIN_ID) && |
| errorMessage.contains(GradleDetector.OLD_APP_PLUGIN_ID)) { |
| return GradleDetector.APP_PLUGIN_ID; |
| } else if (errorMessage.contains(GradleDetector.LIB_PLUGIN_ID) && |
| errorMessage.contains(GradleDetector.OLD_LIB_PLUGIN_ID)) { |
| return GradleDetector.LIB_PLUGIN_ID; |
| } |
| // "Deprecated: Replace 'packageNameSuffix' with 'applicationIdSuffix'" |
| return findSubstring(errorMessage, " with '", "'"); |
| } else if (issue == COMPATIBILITY) { |
| if (errorMessage.startsWith("Version 5.2.08")) { |
| return findSubstring(errorMessage, "Use version ", " "); |
| } |
| } |
| |
| return null; |
| } |
| |
| private static boolean isNumberString(@Nullable String s) { |
| if (s == null || s.isEmpty()) { |
| return false; |
| } |
| for (int i = 0, n = s.length(); i < n; i++) { |
| if (!Character.isDigit(s.charAt(i))) { |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| protected void checkMethodCall( |
| @NonNull Context context, |
| @NonNull String statement, |
| @Nullable String parent, |
| @NonNull Map<String, String> namedArguments, |
| @SuppressWarnings("UnusedParameters") |
| @NonNull List<String> unnamedArguments, |
| @NonNull Object cookie) { |
| String plugin = namedArguments.get("plugin"); |
| if (statement.equals("apply") && parent == null) { |
| boolean isOldAppPlugin = OLD_APP_PLUGIN_ID.equals(plugin); |
| if (isOldAppPlugin || OLD_LIB_PLUGIN_ID.equals(plugin)) { |
| String replaceWith = isOldAppPlugin ? APP_PLUGIN_ID : LIB_PLUGIN_ID; |
| String message = String.format("'%1$s' is deprecated; use '%2$s' instead", plugin, |
| replaceWith); |
| report(context, cookie, DEPRECATED, message); |
| } |
| } |
| } |
| |
| @Nullable |
| private static PreciseRevision parseRevisionSilently(String versionString) { |
| try { |
| return PreciseRevision.parseRevision(versionString); |
| } catch (Throwable t) { |
| return null; |
| } |
| } |
| |
| private static boolean isModelOlderThan011(@NonNull Context context) { |
| return LintUtils.isModelOlderThan(context.getProject().getGradleProjectModel(), 0, 11, 0); |
| } |
| |
| private static int sMajorBuildTools; |
| private static PreciseRevision sLatestBuildTools; |
| |
| /** Returns the latest build tools installed for the given major version. |
| * We just cache this once; we don't need to be accurate in the sense that if the |
| * user opens the SDK manager and installs a more recent version, we capture this in |
| * the same IDE session. |
| * |
| * @param client the associated client |
| * @param major the major version of build tools to look up (e.g. typically 18, 19, ...) |
| * @return the corresponding highest known revision |
| */ |
| @Nullable |
| private static PreciseRevision getLatestBuildTools(@NonNull LintClient client, int major) { |
| if (major != sMajorBuildTools) { |
| sMajorBuildTools = major; |
| |
| List<PreciseRevision> revisions = Lists.newArrayList(); |
| if (major == 21) { |
| revisions.add(new PreciseRevision(21, 1, 2)); |
| } else if (major == 20) { |
| revisions.add(new PreciseRevision(20)); |
| } else if (major == 19) { |
| revisions.add(new PreciseRevision(19, 1)); |
| } else if (major == 18) { |
| revisions.add(new PreciseRevision(18, 1, 1)); |
| } |
| // The above versions can go stale. |
| // Check if a more recent one is installed. (The above are still useful for |
| // people who haven't updated with the SDK manager recently.) |
| File sdkHome = client.getSdkHome(); |
| if (sdkHome != null) { |
| File[] dirs = new File(sdkHome, FD_BUILD_TOOLS).listFiles(); |
| if (dirs != null) { |
| for (File dir : dirs) { |
| String name = dir.getName(); |
| if (!dir.isDirectory() || !Character.isDigit(name.charAt(0))) { |
| continue; |
| } |
| PreciseRevision v = parseRevisionSilently(name); |
| if (v != null && v.getMajor() == major) { |
| revisions.add(v); |
| } |
| } |
| } |
| } |
| |
| if (!revisions.isEmpty()) { |
| sLatestBuildTools = Collections.max(revisions); |
| } |
| } |
| |
| return sLatestBuildTools; |
| } |
| |
| private void checkTargetCompatibility(Context context, Object cookie) { |
| if (mCompileSdkVersion > 0 && mTargetSdkVersion > 0 |
| && mTargetSdkVersion > mCompileSdkVersion) { |
| // NOTE: Keep this in sync with {@link #getOldValue} and {@link #getNewValue} |
| String message = "The targetSdkVersion (" + mTargetSdkVersion |
| + ") should not be higher than the compileSdkVersion (" |
| + mCompileSdkVersion + ")"; |
| report(context, cookie, DEPENDENCY, message); |
| } |
| } |
| |
| @Nullable |
| private static String getStringLiteralValue(@NonNull String value) { |
| if (value.length() > 2 && (value.startsWith("'") && value.endsWith("'") || |
| value.startsWith("\"") && value.endsWith("\""))) { |
| return value.substring(1, value.length() - 1); |
| } |
| |
| return null; |
| } |
| |
| private static int getIntLiteralValue(@NonNull String value, int defaultValue) { |
| try { |
| return Integer.parseInt(value); |
| } catch (NumberFormatException e) { |
| return defaultValue; |
| } |
| } |
| |
| private static boolean isInteger(String token) { |
| return token.matches("\\d+"); |
| } |
| |
| private static boolean isStringLiteral(String token) { |
| return token.startsWith("\"") && token.endsWith("\"") || |
| token.startsWith("'") && token.endsWith("'"); |
| } |
| |
| private void checkDependency( |
| @NonNull Context context, |
| @NonNull GradleCoordinate dependency, |
| @NonNull Object cookie) { |
| if ("com.android.support".equals(dependency.getGroupId())) { |
| checkSupportLibraries(context, dependency, cookie); |
| if (mMinSdkVersion >= 14 && "appcompat-v7".equals(dependency.getArtifactId()) |
| && mCompileSdkVersion >= 1 && mCompileSdkVersion < 21) { |
| report(context, cookie, DEPENDENCY, |
| "Using the appcompat library when minSdkVersion >= 14 and " |
| + "compileSdkVersion < 21 is not necessary"); |
| } |
| return; |
| } else if ("com.google.android.gms".equals(dependency.getGroupId()) |
| && dependency.getArtifactId() != null) { |
| |
| // 5.2.08 is not supported; special case and warn about this |
| if ("5.2.08".equals(dependency.getFullRevision()) && context.isEnabled(COMPATIBILITY)) { |
| // This specific version is actually a preview version which should |
| // not be used (https://code.google.com/p/android/issues/detail?id=75292) |
| String version = "6.1.11"; |
| // Try to find a more recent available version, if one is available |
| File sdkHome = context.getClient().getSdkHome(); |
| File repository = SdkMavenRepository.GOOGLE.getRepositoryLocation(sdkHome, true); |
| if (repository != null) { |
| GradleCoordinate max = SdkMavenRepository.getHighestInstalledVersion( |
| dependency.getGroupId(), dependency.getArtifactId(), repository, |
| null, false); |
| if (max != null) { |
| if (COMPARE_PLUS_HIGHER.compare(dependency, max) < 0) { |
| version = max.getFullRevision(); |
| } |
| } |
| } |
| String message = String.format("Version `5.2.08` should not be used; the app " |
| + "can not be published with this version. Use version `%1$s` " |
| + "instead.", version); |
| report(context, cookie, COMPATIBILITY, message); |
| } |
| |
| checkPlayServices(context, dependency, cookie); |
| return; |
| } |
| |
| PreciseRevision version = null; |
| Issue issue = DEPENDENCY; |
| if ("com.android.tools.build".equals(dependency.getGroupId()) && |
| "gradle".equals(dependency.getArtifactId())) { |
| try { |
| PreciseRevision v = |
| PreciseRevision.parseRevision(GRADLE_PLUGIN_RECOMMENDED_VERSION); |
| if (!v.isPreview()) { |
| version = getNewerRevision(dependency, v); |
| } |
| } catch (NumberFormatException e) { |
| context.log(e, null); |
| } |
| } else if ("com.google.guava".equals(dependency.getGroupId()) && |
| "guava".equals(dependency.getArtifactId())) { |
| version = getNewerRevision(dependency, new PreciseRevision(18, 0)); |
| } else if ("com.google.code.gson".equals(dependency.getGroupId()) && |
| "gson".equals(dependency.getArtifactId())) { |
| version = getNewerRevision(dependency, new PreciseRevision(2, 3)); |
| } else if ("org.apache.httpcomponents".equals(dependency.getGroupId()) && |
| "httpclient".equals(dependency.getArtifactId())) { |
| version = getNewerRevision(dependency, new PreciseRevision(4, 3, 5)); |
| } |
| |
| // Network check for really up to date libraries? Only done in batch mode |
| if (context.getScope().size() > 1 && context.isEnabled(REMOTE_VERSION)) { |
| PreciseRevision latest = getLatestVersionFromRemoteRepo(context.getClient(), dependency, |
| dependency.isPreview()); |
| if (latest != null && isOlderThan(dependency, latest.getMajor(), latest.getMinor(), |
| latest.getMicro())) { |
| version = latest; |
| issue = REMOTE_VERSION; |
| } |
| } |
| |
| if (version != null) { |
| String message = getNewerVersionAvailableMessage(dependency, version); |
| report(context, cookie, issue, message); |
| } |
| } |
| |
| private static String getNewerVersionAvailableMessage(GradleCoordinate dependency, |
| PreciseRevision version) { |
| return getNewerVersionAvailableMessage(dependency, version.toString()); |
| } |
| |
| private static String getNewerVersionAvailableMessage(GradleCoordinate dependency, |
| String version) { |
| // NOTE: Keep this in sync with {@link #getOldValue} and {@link #getNewValue} |
| return "A newer version of " + dependency.getGroupId() + ":" + |
| dependency.getArtifactId() + " than " + dependency.getFullRevision() + |
| " is available: " + version; |
| } |
| |
| /** TODO: Cache these results somewhere! */ |
| @Nullable |
| public static PreciseRevision getLatestVersionFromRemoteRepo(@NonNull LintClient client, |
| @NonNull GradleCoordinate dependency, boolean allowPreview) { |
| return getLatestVersionFromRemoteRepo(client, dependency, true, allowPreview); |
| } |
| |
| @Nullable |
| private static PreciseRevision getLatestVersionFromRemoteRepo(@NonNull LintClient client, |
| @NonNull GradleCoordinate dependency, boolean firstRowOnly, boolean allowPreview) { |
| StringBuilder query = new StringBuilder(); |
| String encoding = UTF_8.name(); |
| try { |
| query.append("http://search.maven.org/solrsearch/select?q=g:%22"); |
| query.append(URLEncoder.encode(dependency.getGroupId(), encoding)); |
| query.append("%22+AND+a:%22"); |
| query.append(URLEncoder.encode(dependency.getArtifactId(), encoding)); |
| } catch (UnsupportedEncodingException ee) { |
| return null; |
| } |
| query.append("%22&core=gav"); |
| if (firstRowOnly) { |
| query.append("&rows=1"); |
| } |
| query.append("&wt=json"); |
| |
| String response = readUrlData(client, dependency, query.toString()); |
| if (response == null) { |
| return null; |
| } |
| |
| // Sample response: |
| // { |
| // "responseHeader": { |
| // "status": 0, |
| // "QTime": 0, |
| // "params": { |
| // "fl": "id,g,a,v,p,ec,timestamp,tags", |
| // "sort": "score desc,timestamp desc,g asc,a asc,v desc", |
| // "indent": "off", |
| // "q": "g:\"com.google.guava\" AND a:\"guava\"", |
| // "core": "gav", |
| // "wt": "json", |
| // "rows": "1", |
| // "version": "2.2" |
| // } |
| // }, |
| // "response": { |
| // "numFound": 37, |
| // "start": 0, |
| // "docs": [{ |
| // "id": "com.google.guava:guava:17.0", |
| // "g": "com.google.guava", |
| // "a": "guava", |
| // "v": "17.0", |
| // "p": "bundle", |
| // "timestamp": 1398199666000, |
| // "tags": ["spec", "libraries", "classes", "google", "code"], |
| // "ec": ["-javadoc.jar", "-sources.jar", ".jar", "-site.jar", ".pom"] |
| // }] |
| // } |
| // } |
| |
| // Look for version info: This is just a cheap skim of the above JSON results |
| boolean foundPreview = false; |
| int index = response.indexOf("\"response\""); //$NON-NLS-1$ |
| while (index != -1) { |
| index = response.indexOf("\"v\":", index); //$NON-NLS-1$ |
| if (index != -1) { |
| index += 4; |
| int start = response.indexOf('"', index) + 1; |
| int end = response.indexOf('"', start + 1); |
| if (end > start && start >= 0) { |
| PreciseRevision revision = parseRevisionSilently(response.substring(start, end)); |
| if (revision != null) { |
| foundPreview = revision.isPreview(); |
| if (allowPreview || !foundPreview) { |
| return revision; |
| } |
| } |
| } |
| } |
| } |
| |
| if (!allowPreview && foundPreview && firstRowOnly) { |
| // Recurse: search more than the first row this time to see if we can find a |
| // non-preview version |
| return getLatestVersionFromRemoteRepo(client, dependency, false, false); |
| } |
| |
| return null; |
| } |
| |
| /** Normally null; used for testing */ |
| @Nullable |
| @VisibleForTesting |
| static Map<String,String> sMockData; |
| |
| @Nullable |
| private static String readUrlData( |
| @NonNull LintClient client, |
| @NonNull GradleCoordinate dependency, |
| @NonNull String query) { |
| // For unit testing: avoid network as well as unexpected new versions |
| if (sMockData != null) { |
| String value = sMockData.get(query); |
| assert value != null : query; |
| return value; |
| } |
| |
| try { |
| URL url = new URL(query); |
| |
| URLConnection connection = client.openConnection(url); |
| if (connection == null) { |
| return null; |
| } |
| try { |
| InputStream is = connection.getInputStream(); |
| if (is == null) { |
| return null; |
| } |
| BufferedReader reader = new BufferedReader(new InputStreamReader(is, UTF_8)); |
| try { |
| StringBuilder sb = new StringBuilder(500); |
| String line; |
| while ((line = reader.readLine()) != null) { |
| sb.append(line); |
| sb.append('\n'); |
| } |
| |
| return sb.toString(); |
| } finally { |
| reader.close(); |
| } |
| } finally { |
| client.closeConnection(connection); |
| } |
| } catch (IOException ioe) { |
| client.log(ioe, "Could not connect to maven central to look up the " + |
| "latest available version for %1$s", dependency); |
| return null; |
| } |
| } |
| |
| private boolean checkGradlePluginDependency(Context context, GradleCoordinate dependency, |
| Object cookie) { |
| GradleCoordinate latestPlugin = GradleCoordinate.parseCoordinateString( |
| SdkConstants.GRADLE_PLUGIN_NAME + |
| GRADLE_PLUGIN_MINIMUM_VERSION); |
| if (GradleCoordinate.COMPARE_PLUS_HIGHER.compare(dependency, latestPlugin) < 0) { |
| String message = "You must use a newer version of the Android Gradle plugin. The " |
| + "minimum supported version is " + GRADLE_PLUGIN_MINIMUM_VERSION + |
| " and the recommended version is " + GRADLE_PLUGIN_RECOMMENDED_VERSION; |
| report(context, cookie, GRADLE_PLUGIN_COMPATIBILITY, message); |
| return true; |
| } |
| return false; |
| } |
| |
| private void checkSupportLibraries(Context context, GradleCoordinate dependency, |
| Object cookie) { |
| String groupId = dependency.getGroupId(); |
| String artifactId = dependency.getArtifactId(); |
| assert groupId != null && artifactId != null; |
| |
| // See if the support library version is lower than the targetSdkVersion |
| if (mTargetSdkVersion > 0 && dependency.getMajorVersion() < mTargetSdkVersion && |
| dependency.getMajorVersion() != GradleCoordinate.PLUS_REV_VALUE && |
| // The multidex library doesn't follow normal supportlib numbering scheme |
| !dependency.getArtifactId().startsWith("multidex") && |
| context.isEnabled(COMPATIBILITY)) { |
| String message = "This support library should not use a lower version (" |
| + dependency.getMajorVersion() + ") than the `targetSdkVersion` (" |
| + mTargetSdkVersion + ")"; |
| report(context, cookie, COMPATIBILITY, message); |
| } |
| |
| // Check to make sure you have the Android support repository installed |
| File sdkHome = context.getClient().getSdkHome(); |
| File repository = SdkMavenRepository.ANDROID.getRepositoryLocation(sdkHome, true); |
| if (repository == null) { |
| report(context, cookie, DEPENDENCY, |
| "Dependency on a support library, but the SDK installation does not " |
| + "have the \"Extras > Android Support Repository\" installed. " |
| + "Open the SDK manager and install it."); |
| } else { |
| checkLocalMavenVersions(context, dependency, cookie, groupId, artifactId, |
| repository); |
| } |
| } |
| |
| private void checkPlayServices(Context context, GradleCoordinate dependency, Object cookie) { |
| String groupId = dependency.getGroupId(); |
| String artifactId = dependency.getArtifactId(); |
| assert groupId != null && artifactId != null; |
| |
| File sdkHome = context.getClient().getSdkHome(); |
| File repository = SdkMavenRepository.GOOGLE.getRepositoryLocation(sdkHome, true); |
| if (repository == null) { |
| report(context, cookie, DEPENDENCY, |
| "Dependency on Play Services, but the SDK installation does not " |
| + "have the \"Extras > Google Repository\" installed. " |
| + "Open the SDK manager and install it."); |
| } else { |
| checkLocalMavenVersions(context, dependency, cookie, groupId, artifactId, |
| repository); |
| } |
| } |
| |
| private void checkLocalMavenVersions(Context context, GradleCoordinate dependency, |
| Object cookie, String groupId, String artifactId, File repository) { |
| GradleCoordinate max = SdkMavenRepository.getHighestInstalledVersion(groupId, artifactId, |
| repository, null, false); |
| if (max != null) { |
| if (COMPARE_PLUS_HIGHER.compare(dependency, max) < 0 |
| && context.isEnabled(DEPENDENCY)) { |
| String message = getNewerVersionAvailableMessage(dependency, max.getFullRevision()); |
| report(context, cookie, DEPENDENCY, message); |
| } |
| } |
| } |
| |
| private static PreciseRevision getNewerRevision(@NonNull GradleCoordinate dependency, |
| @NonNull PreciseRevision revision) { |
| assert dependency.getGroupId() != null; |
| assert dependency.getArtifactId() != null; |
| GradleCoordinate coordinate; |
| if (revision.isPreview()) { |
| String coordinateString = dependency.getGroupId() |
| + ":" + dependency.getArtifactId() |
| + ":" + revision.toString(); |
| coordinate = GradleCoordinate.parseCoordinateString(coordinateString); |
| } else { |
| coordinate = new GradleCoordinate(dependency.getGroupId(), dependency.getArtifactId(), |
| revision.getMajor(), revision.getMinor(), revision.getMicro()); |
| } |
| if (COMPARE_PLUS_HIGHER.compare(dependency, coordinate) < 0) { |
| return revision; |
| } else { |
| return null; |
| } |
| } |
| |
| private static boolean isOlderThan(@NonNull GradleCoordinate dependency, int major, int minor, |
| int micro) { |
| assert dependency.getGroupId() != null; |
| assert dependency.getArtifactId() != null; |
| return COMPARE_PLUS_HIGHER.compare(dependency, |
| new GradleCoordinate(dependency.getGroupId(), |
| dependency.getArtifactId(), major, minor, micro)) < 0; |
| } |
| |
| private void report(@NonNull Context context, @NonNull Object cookie, @NonNull Issue issue, |
| @NonNull String message) { |
| if (context.isEnabled(issue)) { |
| // Suppressed? |
| // Temporarily unconditionally checking for suppress comments in Gradle files |
| // since Studio insists on an AndroidLint id prefix |
| boolean checkComments = /*context.getClient().checkForSuppressComments() |
| &&*/ context.containsCommentSuppress(); |
| if (checkComments) { |
| int startOffset = getStartOffset(context, cookie); |
| if (startOffset >= 0 && context.isSuppressedWithComment(startOffset, issue)) { |
| return; |
| } |
| } |
| |
| context.report(issue, createLocation(context, cookie), message); |
| } |
| } |
| |
| @SuppressWarnings("MethodMayBeStatic") |
| @NonNull |
| protected Object getPropertyKeyCookie(@NonNull Object cookie) { |
| return cookie; |
| } |
| |
| @SuppressWarnings({"MethodMayBeStatic", "UnusedDeclaration"}) |
| @NonNull |
| protected Object getPropertyPairCookie(@NonNull Object cookie) { |
| return cookie; |
| } |
| |
| @SuppressWarnings("MethodMayBeStatic") |
| protected int getStartOffset(@NonNull Context context, @NonNull Object cookie) { |
| return -1; |
| } |
| |
| @SuppressWarnings({"MethodMayBeStatic", "UnusedParameters"}) |
| protected Location createLocation(@NonNull Context context, @NonNull Object cookie) { |
| return null; |
| } |
| } |