All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.android.tools.lint.checks.GradleDetector Maven / Gradle / Ivy

/*
 * 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.FD_EXTRAS;
import static com.android.SdkConstants.FD_M2_REPOSITORY;
import static com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_HIGHER;
import static com.android.tools.lint.checks.ManifestDetector.TARGET_NEWER;
import static java.io.File.separator;
import static java.io.File.separatorChar;

import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.common.repository.GradleCoordinate;
import com.android.ide.common.sdk.SdkVersionInfo;
import com.android.sdklib.repository.FullRevision;
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.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.google.common.collect.Lists;

import java.io.File;
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",
            "Looks for old or obsolete Gradle library dependencies",
            "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);

    /** Invalid or dangerous paths */
    public static final Issue PATH = Issue.create(
            "GradlePath", //$NON-NLS-1$
            "Gradle Path Issues",
            "Looks for Gradle path problems such as using platform specific path separators",
            "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",
            "Looks for constructs in Gradle files which affect IDE usage",
            "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",
            "Looks for dependencies using a dynamic version rather than a fixed 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).setEnabledByDefault(false);

    /** 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",
            "Identifies accidental calls to implicit getters",
            "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",
            "Ensures that tool and library versions are compatible",

            "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);

    private int mCompileSdkVersion;
    private int mTargetSdkVersion;

    @Override
    public boolean appliesTo(@NonNull Context context, @NonNull File file) {
        return true;
    }

    @NonNull
    @Override
    public Speed getSpeed() {
        return Speed.NORMAL;
    }

    // ---- Implements Detector.GradleScanner ----

    @Override
    public void visitBuildScript(@NonNull Context context, Map sharedData) {
    }

    @SuppressWarnings("UnusedDeclaration")
    protected static boolean isInterestingBlock(@NonNull String parent) {
        return parent.equals("defaultConfig")
                || parent.equals("android")
                || parent.equals("dependencies");
    }

    @SuppressWarnings("UnusedDeclaration")
    protected static boolean isInterestingProperty(@NonNull String property,
            @SuppressWarnings("UnusedParameters") @NonNull String parent) {
        return property.equals("targetSdkVersion")
                || property.equals("buildToolsVersion")
                || property.equals("compile")
                || property.equals("debugCompile")
                || property.equals("classpath")
                || property.equals("versionName")
                || property.equals("versionCode")
                || property.equals("compileSdkVersion");
    }

    /** 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,
        @NonNull Object cookie) {
        if (parent.equals("defaultConfig")) {
            if (property.equals("targetSdkVersion")) {
                int version = getIntLiteralValue(value, -1);
                if (version > 0 && version < SdkVersionInfo.HIGHEST_KNOWN_API) {
                    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, cookie, TARGET_NEWER, message);
                }
                if (version > 0) {
                    mTargetSdkVersion = version;
                    checkTargetCompatibility(context, cookie);
                }
            }
            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, cookie, GRADLE_GETTER, message);

                }
            }
        } else if (property.equals("compileSdkVersion") && parent.equals("android")) {
            int version = getIntLiteralValue(value, -1);
            if (version > 0) {
                mCompileSdkVersion = version;
                checkTargetCompatibility(context, cookie);
            }
        } else if (property.equals("buildToolsVersion") && parent.equals("android")) {
            String versionString = getStringLiteralValue(value);
            if (versionString != null) {
                FullRevision version = FullRevision.parseRevision(versionString);
                FullRevision recommended = getLatestBuildTools(context.getClient(),
                        version.getMajor());
                if (recommended != null && version.compareTo(recommended) < 0) {
                    String message = "Old buildToolsVersion; recommended version "
                            + "is " + recommended + " or later";
                    report(context, cookie, DEPENDENCY, message);
                }
            }
        } else if (parent.equals("dependencies") &&
                (property.equals("compile")
                        || property.equals("debugCompile")
                        || property.equals("classpath"))) {
            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, cookie, PATH, message);

                } else if (new File(path.replace('/', File.separatorChar)).isAbsolute()) {
                    String message = "Avoid using absolute paths in .gradle files";
                    report(context, cookie, PATH, message);
                }
            } else {
                String dependency = getStringLiteralValue(value);
                if (dependency != null) {
                    GradleCoordinate gc = GradleCoordinate.parseCoordinateString(dependency);
                    if (gc != null) {
                        checkDependency(context, gc, cookie);

                        if (!dependency.startsWith(SdkConstants.GRADLE_PLUGIN_NAME)
                                && gc.acceptsGreaterRevisions()) {
                            String message = "Avoid using + in version numbers; can lead "
                                    + "to unpredictable and  unrepeatable builds";
                            report(context, cookie, PLUS, message);
                        }
                    }
                }
            }
        }
    }

    private static int sMajorBuildTools;
    private static FullRevision 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 FullRevision getLatestBuildTools(@NonNull LintClient client, int major) {
        if (major != sMajorBuildTools) {
            sMajorBuildTools = major;

            List revisions = Lists.newArrayList();
            if (major == 19) {
                revisions.add(new FullRevision(19, 0, 1));
            } else if (major == 18) {
                revisions.add(new FullRevision(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;
                        }
                        FullRevision v = FullRevision.parseRevision(name);
                        if (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) {
            String message = "The targetSdkVersion should not be higher than the compileSdkVersion";
            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()) &&
                ("support-v4".equals(dependency.getArtifactId()) ||
                        "appcompat-v7".equals(dependency.getArtifactId()))) {
            checkSupportLibraries(context, dependency, cookie);
            return;
        } else if ("com.google.android.gms".equals(dependency.getGroupId()) &&
                "play-services".equals(dependency.getArtifactId())) {
            checkPlayServices(context, dependency, cookie);
            return;
        }

        boolean isObsolete = false;
        if ("com.android.tools.build".equals(dependency.getGroupId()) &&
                "gradle".equals(dependency.getArtifactId())) {
            if (isOlderThan(dependency, 0, 7, 3)) {
                isObsolete = true;
            }
        } else if ("com.google.guava".equals(dependency.getGroupId()) &&
                "guava".equals(dependency.getArtifactId())) {
            if (isOlderThan(dependency, 15, 0, 0)) {
                isObsolete = true;
            }
        } else if ("com.google.code.gson".equals(dependency.getGroupId()) &&
                "gson".equals(dependency.getArtifactId())) {
            if (isOlderThan(dependency, 2, 2, 4)) {
                isObsolete = true;
            }
        } else if ("org.apache.httpcomponents".equals(dependency.getGroupId()) &&
                "httpclient".equals(dependency.getArtifactId())) {
            if (isOlderThan(dependency, 4, 3, 1)) {
                isObsolete = true;
            }
        }

        if (isObsolete) {
            String message = "A newer version of " + dependency.getGroupId() + ":" +
                    dependency.getArtifactId() + " than " + dependency.getFullRevision() +
                    " is available";
            report(context, cookie, DEPENDENCY, message);
        }
    }

    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 &&
                context.isEnabled(COMPATIBILITY)) {
            String message = "The support library should not use a lower version ("
                + dependency.getMajorVersion() + ") than the targetSdkVersion ("
                    + mCompileSdkVersion + ")";
            report(context, cookie, COMPATIBILITY, message);
        }

        // Check to make sure you have the Android support repository installed
        File repository = findRepository(context.getClient(), "android");
        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 repository = findRepository(context.getClient(), "google");
        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 = getHighestInstalledVersion(groupId, artifactId, repository);
        if (max != null) {
            if (COMPARE_PLUS_HIGHER.compare(dependency, max) < 0
                    && context.isEnabled(DEPENDENCY)) {
                String message = "A newer version of " + groupId
                        + ":" + artifactId + " than " +
                        dependency.getFullRevision() + " is available: " +
                        max.getFullRevision();
                report(context, cookie, DEPENDENCY, message);
            }
        }
    }

    private static File findRepository(LintClient client, String extrasName) {
        File sdkHome = client.getSdkHome();
        if (sdkHome != null) {
            File repository = new File(sdkHome, FD_EXTRAS + separator + extrasName + separator
                    + FD_M2_REPOSITORY);
            if (repository.exists()) {
                return repository;
            }
        }

        return null;
    }

    @Nullable
    private static GradleCoordinate getHighestInstalledVersion(
            @NonNull String groupId,
            @NonNull String artifactId,
            @NonNull File repository) {
        File versionDir = new File(repository,
                groupId.replace('.', separatorChar) + separator + artifactId);
        File[] versions = versionDir.listFiles();
        if (versions != null) {
            List versionCoordinates = Lists.newArrayList();
            for (File dir : versions) {
                if (!dir.isDirectory()) {
                    continue;
                }
                GradleCoordinate gc = GradleCoordinate.parseCoordinateString(
                        groupId + ":" + artifactId + ":" + dir.getName());
                if (gc != null) {
                    versionCoordinates.add(gc);
                }
            }
            if (!versionCoordinates.isEmpty()) {
                return Collections.max(versionCoordinates, COMPARE_PLUS_HIGHER);
            }
        }

        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, null);
        }
    }

    @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;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy