diff options
author | Raphael Moll <ralf@android.com> | 2013-02-05 11:01:35 -0800 |
---|---|---|
committer | Raphael Moll <ralf@android.com> | 2013-02-06 19:56:02 -0800 |
commit | 3cf16960a67f0c8178e7bc81988fb1436e775e91 (patch) | |
tree | caad2fa1366fc15d09c679476ae9caf5ed9a6596 /lint | |
parent | b15ea7875a9a4101d4be7e547950b7d2b64c1464 (diff) | |
download | sdk-3cf16960a67f0c8178e7bc81988fb1436e775e91.zip sdk-3cf16960a67f0c8178e7bc81988fb1436e775e91.tar.gz sdk-3cf16960a67f0c8178e7bc81988fb1436e775e91.tar.bz2 |
Remove source of prebuilts.
Sources are now located in tools/base.git.
Change-Id: I9cbe1deb98f8c43e90f5fb04b668f664b9850620
Diffstat (limited to 'lint')
118 files changed, 0 insertions, 38325 deletions
diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/AsmVisitor.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/AsmVisitor.java deleted file mode 100644 index e8a4bb5..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/AsmVisitor.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2012 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.client.api; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.google.common.annotations.Beta; - -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Specialized visitor for running detectors on a class object model. - * <p> - * It operates in two phases: - * <ol> - * <li> First, it computes a set of maps where it generates a map from each - * significant method name to a list of detectors to consult for that method - * name. The set of method names that a detector is interested in is provided - * by the detectors themselves. - * <li> Second, it iterates over the DOM a single time. For each method call it finds, - * it dispatches to any check that has registered interest in that method name. - * <li> Finally, it runs a full check on those class scanners that do not register - * specific method names to be checked. This is intended for those detectors - * that do custom work, not related specifically to method calls. - * </ol> - * It also notifies all the detectors before and after the document is processed - * such that they can do pre- and post-processing. - * <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 -class AsmVisitor { - /** - * Number of distinct node types specified in {@link AbstractInsnNode}. Sadly - * there isn't a max-constant there, so update this along with ASM library - * updates. - */ - private static final int TYPE_COUNT = AbstractInsnNode.LINE + 1; - private final Map<String, List<ClassScanner>> mMethodNameToChecks = - new HashMap<String, List<ClassScanner>>(); - private final Map<String, List<ClassScanner>> mMethodOwnerToChecks = - new HashMap<String, List<ClassScanner>>(); - private final List<Detector> mFullClassChecks = new ArrayList<Detector>(); - - private final List<? extends Detector> mAllDetectors; - private List<ClassScanner>[] mNodeTypeDetectors; - - // Really want this: - //<T extends List<Detector> & Detector.ClassScanner> ClassVisitor(T xmlDetectors) { - // but it makes client code tricky and ugly. - @SuppressWarnings("unchecked") - AsmVisitor(@NonNull LintClient client, @NonNull List<? extends Detector> classDetectors) { - mAllDetectors = classDetectors; - - // TODO: Check appliesTo() for files, and find a quick way to enable/disable - // rules when running through a full project! - for (Detector detector : classDetectors) { - Detector.ClassScanner scanner = (Detector.ClassScanner) detector; - - boolean checkFullClass = true; - - Collection<String> names = scanner.getApplicableCallNames(); - if (names != null) { - checkFullClass = false; - for (String element : names) { - List<Detector.ClassScanner> list = mMethodNameToChecks.get(element); - if (list == null) { - list = new ArrayList<Detector.ClassScanner>(); - mMethodNameToChecks.put(element, list); - } - list.add(scanner); - } - } - - Collection<String> owners = scanner.getApplicableCallOwners(); - if (owners != null) { - checkFullClass = false; - for (String element : owners) { - List<Detector.ClassScanner> list = mMethodOwnerToChecks.get(element); - if (list == null) { - list = new ArrayList<Detector.ClassScanner>(); - mMethodOwnerToChecks.put(element, list); - } - list.add(scanner); - } - } - - int[] types = scanner.getApplicableAsmNodeTypes(); - if (types != null) { - checkFullClass = false; - for (int type : types) { - if (type < 0 || type >= TYPE_COUNT) { - // Can't support this node type: looks like ASM wasn't updated correctly. - client.log(null, "Out of range node type %1$d from detector %2$s", - type, scanner); - continue; - } - if (mNodeTypeDetectors == null) { - mNodeTypeDetectors = new List[TYPE_COUNT]; - } - List<ClassScanner> checks = mNodeTypeDetectors[type]; - if (checks == null) { - checks = new ArrayList<ClassScanner>(); - mNodeTypeDetectors[type] = checks; - } - checks.add(scanner); - } - } - - if (checkFullClass) { - mFullClassChecks.add(detector); - } - } - } - - @SuppressWarnings("rawtypes") // ASM API uses raw types - void runClassDetectors(ClassContext context) { - ClassNode classNode = context.getClassNode(); - - for (Detector detector : mAllDetectors) { - detector.beforeCheckFile(context); - } - - for (Detector detector : mFullClassChecks) { - Detector.ClassScanner scanner = (Detector.ClassScanner) detector; - scanner.checkClass(context, classNode); - detector.afterCheckFile(context); - } - - if (!mMethodNameToChecks.isEmpty() || !mMethodOwnerToChecks.isEmpty() || - mNodeTypeDetectors != null && mNodeTypeDetectors.length > 0) { - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - InsnList nodes = method.instructions; - for (int i = 0, n = nodes.size(); i < n; i++) { - AbstractInsnNode instruction = nodes.get(i); - int type = instruction.getType(); - if (type == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode call = (MethodInsnNode) instruction; - - String owner = call.owner; - List<ClassScanner> scanners = mMethodOwnerToChecks.get(owner); - if (scanners != null) { - for (ClassScanner scanner : scanners) { - scanner.checkCall(context, classNode, method, call); - } - } - - String name = call.name; - scanners = mMethodNameToChecks.get(name); - if (scanners != null) { - for (ClassScanner scanner : scanners) { - scanner.checkCall(context, classNode, method, call); - } - } - } - - if (mNodeTypeDetectors != null && type < mNodeTypeDetectors.length) { - List<ClassScanner> scanners = mNodeTypeDetectors[type]; - if (scanners != null) { - for (ClassScanner scanner : scanners) { - scanner.checkInstruction(context, classNode, method, instruction); - } - } - } - } - } - } - - for (Detector detector : mAllDetectors) { - detector.afterCheckFile(context); - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/CircularDependencyException.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/CircularDependencyException.java deleted file mode 100644 index 337eb27..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/CircularDependencyException.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2012 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -import com.google.common.annotations.Beta; - -/** - * Exception thrown when there is a circular dependency, such as a circular dependency - * of library mProject references - * <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 class CircularDependencyException extends RuntimeException { - @Nullable - private Project mProject; - - @Nullable - private Location mLocation; - - CircularDependencyException(@NonNull String message) { - super(message); - } - - /** - * Returns the associated project, if any - * - * @return the associated project, if any - */ - @Nullable - public Project getProject() { - return mProject; - } - - /** - * Sets the associated project, if any - * - * @param project the associated project, if any - */ - public void setProject(@Nullable Project project) { - mProject = project; - } - - /** - * Returns the associated location, if any - * - * @return the associated location, if any - */ - @Nullable - public Location getLocation() { - return mLocation; - } - - /** - * Sets the associated location, if any - * - * @param location the associated location, if any - */ - public void setLocation(@Nullable Location location) { - mLocation = location; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/Configuration.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/Configuration.java deleted file mode 100644 index d233be7..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/Configuration.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Severity; -import com.google.common.annotations.Beta; - -/** - * Lint configuration for an Android project such as which specific rules to include, - * which specific rules to exclude, and which specific errors to ignore. - * <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 Configuration { - /** - * Checks whether this issue should be ignored because the user has already - * suppressed the error? Note that this refers to individual issues being - * suppressed/ignored, not a whole detector being disabled via something - * like {@link #isEnabled(Issue)}. - * - * @param context the context used by the detector when the issue was found - * @param issue the issue that was found - * @param location the location of the issue - * @param message the associated user message - * @param data additional information about an issue (see - * {@link LintClient#report(Context, Issue, Severity, Location, String, Object)} - * for more information - * @return true if this issue should be suppressed - */ - public boolean isIgnored( - @NonNull Context context, - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - return false; - } - - /** - * Returns false if the given issue has been disabled. This is just - * a convenience method for {@code getSeverity(issue) != Severity.IGNORE}. - * - * @param issue the issue to check - * @return false if the issue has been disabled - */ - public boolean isEnabled(@NonNull Issue issue) { - return getSeverity(issue) != Severity.IGNORE; - } - - /** - * Returns the severity for a given issue. This is the same as the - * {@link Issue#getDefaultSeverity()} unless the user has selected a custom - * severity (which is tool context dependent). - * - * @param issue the issue to look up the severity from - * @return the severity use for issues for the given detector - */ - public Severity getSeverity(@NonNull Issue issue) { - return issue.getDefaultSeverity(); - } - - // Editing configurations - - /** - * Marks the given warning as "ignored". - * - * @param context The scanning context - * @param issue the issue to be ignored - * @param location The location to ignore the warning at, if any - * @param message The message for the warning - * @param data The corresponding data, or null - */ - public abstract void ignore( - @NonNull Context context, - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data); - - /** - * Sets the severity to be used for this issue. - * - * @param issue the issue to set the severity for - * @param severity the severity to associate with this issue, or null to - * reset the severity to the default - */ - public abstract void setSeverity(@NonNull Issue issue, @Nullable Severity severity); - - // Bulk editing support - - /** - * Marks the beginning of a "bulk" editing operation with repeated calls to - * {@link #setSeverity} or {@link #ignore}. After all the values have been - * set, the client <b>must</b> call {@link #finishBulkEditing()}. This - * allows configurations to avoid doing expensive I/O (such as writing out a - * config XML file) for each and every editing operation when they are - * applied in bulk, such as from a configuration dialog's "Apply" action. - */ - public void startBulkEditing() { - } - - /** - * Marks the end of a "bulk" editing operation, where values should be - * committed to persistent storage. See {@link #startBulkEditing()} for - * details. - */ - public void finishBulkEditing() { - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultConfiguration.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultConfiguration.java deleted file mode 100644 index d6b925c..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultConfiguration.java +++ /dev/null @@ -1,462 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.Severity; -import com.google.common.annotations.Beta; -import com.google.common.io.Closeables; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXParseException; - -import java.io.BufferedInputStream; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -/** - * Default implementation of a {@link Configuration} which reads and writes - * configuration data into {@code lint.xml} in the project directory. - * <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 class DefaultConfiguration extends Configuration { - private final LintClient mClient; - private static final String CONFIG_FILE_NAME = "lint.xml"; //$NON-NLS-1$ - - // Lint XML File - @NonNull - private static final String TAG_ISSUE = "issue"; //$NON-NLS-1$ - @NonNull - private static final String ATTR_ID = "id"; //$NON-NLS-1$ - @NonNull - private static final String ATTR_SEVERITY = "severity"; //$NON-NLS-1$ - @NonNull - private static final String ATTR_PATH = "path"; //$NON-NLS-1$ - @NonNull - private static final String TAG_IGNORE = "ignore"; //$NON-NLS-1$ - @NonNull - private static final String VALUE_ALL = "all"; //$NON-NLS-1$ - - private final Configuration mParent; - private final Project mProject; - private final File mConfigFile; - private boolean mBulkEditing; - - /** Map from id to list of project-relative paths for suppressed warnings */ - private Map<String, List<String>> mSuppressed; - - /** - * Map from id to custom {@link Severity} override - */ - private Map<String, Severity> mSeverity; - - protected DefaultConfiguration( - @NonNull LintClient client, - @Nullable Project project, - @Nullable Configuration parent, - @NonNull File configFile) { - mClient = client; - mProject = project; - mParent = parent; - mConfigFile = configFile; - } - - protected DefaultConfiguration( - @NonNull LintClient client, - @NonNull Project project, - @Nullable Configuration parent) { - this(client, project, parent, new File(project.getDir(), CONFIG_FILE_NAME)); - } - - /** - * Creates a new {@link DefaultConfiguration} - * - * @param client the client to report errors to etc - * @param project the associated project - * @param parent the parent/fallback configuration or null - * @return a new configuration - */ - @NonNull - public static DefaultConfiguration create( - @NonNull LintClient client, - @NonNull Project project, - @Nullable Configuration parent) { - return new DefaultConfiguration(client, project, parent); - } - - /** - * Creates a new {@link DefaultConfiguration} for the given lint config - * file, not affiliated with a project. This is used for global - * configurations. - * - * @param client the client to report errors to etc - * @param lintFile the lint file containing the configuration - * @return a new configuration - */ - @NonNull - public static DefaultConfiguration create(@NonNull LintClient client, @NonNull File lintFile) { - return new DefaultConfiguration(client, null /*project*/, null /*parent*/, lintFile); - } - - @Override - public boolean isIgnored( - @NonNull Context context, - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - ensureInitialized(); - - String id = issue.getId(); - List<String> paths = mSuppressed.get(id); - if (paths == null) { - paths = mSuppressed.get(VALUE_ALL); - } - if (paths != null && location != null) { - File file = location.getFile(); - String relativePath = context.getProject().getRelativePath(file); - for (String suppressedPath : paths) { - if (suppressedPath.equals(relativePath)) { - return true; - } - } - } - - if (mParent != null) { - return mParent.isIgnored(context, issue, location, message, data); - } - - return false; - } - - @NonNull - protected Severity getDefaultSeverity(@NonNull Issue issue) { - if (!issue.isEnabledByDefault()) { - return Severity.IGNORE; - } - - return issue.getDefaultSeverity(); - } - - @Override - @NonNull - public Severity getSeverity(@NonNull Issue issue) { - ensureInitialized(); - - Severity severity = mSeverity.get(issue.getId()); - if (severity == null) { - severity = mSeverity.get(VALUE_ALL); - } - - if (severity != null) { - return severity; - } - - if (mParent != null) { - return mParent.getSeverity(issue); - } - - return getDefaultSeverity(issue); - } - - private void ensureInitialized() { - if (mSuppressed == null) { - readConfig(); - } - } - - private void formatError(String message, Object... args) { - if (args != null && args.length > 0) { - message = String.format(message, args); - } - message = "Failed to parse lint.xml configuration file: " + message; - LintDriver driver = new LintDriver(new IssueRegistry() { - @Override @NonNull public List<Issue> getIssues() { - return Collections.emptyList(); - } - }, mClient); - mClient.report(new Context(driver, mProject, mProject, mConfigFile), - IssueRegistry.LINT_ERROR, - mProject.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), - Location.create(mConfigFile), message, null); - } - - private void readConfig() { - mSuppressed = new HashMap<String, List<String>>(); - mSeverity = new HashMap<String, Severity>(); - - if (!mConfigFile.exists()) { - return; - } - - @SuppressWarnings("resource") // Eclipse doesn't know about Closeables.closeQuietly - BufferedInputStream input = null; - try { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - input = new BufferedInputStream(new FileInputStream(mConfigFile)); - InputSource source = new InputSource(input); - factory.setNamespaceAware(false); - factory.setValidating(false); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(source); - NodeList issues = document.getElementsByTagName(TAG_ISSUE); - for (int i = 0, count = issues.getLength(); i < count; i++) { - Node node = issues.item(i); - Element element = (Element) node; - String id = element.getAttribute(ATTR_ID); - if (id.isEmpty()) { - formatError("Invalid lint config file: Missing required issue id attribute"); - continue; - } - - NamedNodeMap attributes = node.getAttributes(); - for (int j = 0, n = attributes.getLength(); j < n; j++) { - Node attribute = attributes.item(j); - String name = attribute.getNodeName(); - String value = attribute.getNodeValue(); - if (ATTR_ID.equals(name)) { - // already handled - } else if (ATTR_SEVERITY.equals(name)) { - for (Severity severity : Severity.values()) { - if (value.equalsIgnoreCase(severity.name())) { - mSeverity.put(id, severity); - break; - } - } - } else { - formatError("Unexpected attribute \"%1$s\"", name); - } - } - - // Look up ignored errors - NodeList childNodes = element.getChildNodes(); - if (childNodes.getLength() > 0) { - for (int j = 0, n = childNodes.getLength(); j < n; j++) { - Node child = childNodes.item(j); - if (child.getNodeType() == Node.ELEMENT_NODE) { - Element ignore = (Element) child; - String path = ignore.getAttribute(ATTR_PATH); - if (path.isEmpty()) { - formatError("Missing required %1$s attribute under %2$s", - ATTR_PATH, id); - } else { - List<String> paths = mSuppressed.get(id); - if (paths == null) { - paths = new ArrayList<String>(n / 2 + 1); - mSuppressed.put(id, paths); - } - paths.add(path); - } - } - } - } - } - } catch (SAXParseException e) { - formatError(e.getMessage()); - } catch (Exception e) { - mClient.log(e, null); - } finally { - Closeables.closeQuietly(input); - } - } - - private void writeConfig() { - try { - // Write the contents to a new file first such that we don't clobber the - // existing file if some I/O error occurs. - File file = new File(mConfigFile.getParentFile(), - mConfigFile.getName() + ".new"); //$NON-NLS-1$ - - Writer writer = new BufferedWriter(new FileWriter(file)); - writer.write( - "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + //$NON-NLS-1$ - "<lint>\n"); //$NON-NLS-1$ - - if (!mSuppressed.isEmpty() || !mSeverity.isEmpty()) { - // Process the maps in a stable sorted order such that if the - // files are checked into version control with the project, - // there are no random diffs just because hashing algorithms - // differ: - Set<String> idSet = new HashSet<String>(); - for (String id : mSuppressed.keySet()) { - idSet.add(id); - } - for (String id : mSeverity.keySet()) { - idSet.add(id); - } - List<String> ids = new ArrayList<String>(idSet); - Collections.sort(ids); - - for (String id : ids) { - writer.write(" <"); //$NON-NLS-1$ - writer.write(TAG_ISSUE); - writeAttribute(writer, ATTR_ID, id); - Severity severity = mSeverity.get(id); - if (severity != null) { - writeAttribute(writer, ATTR_SEVERITY, - severity.name().toLowerCase(Locale.US)); - } - - List<String> paths = mSuppressed.get(id); - if (paths != null && !paths.isEmpty()) { - writer.write('>'); - writer.write('\n'); - // The paths are already kept in sorted order when they are modified - // by ignore(...) - for (String path : paths) { - writer.write(" <"); //$NON-NLS-1$ - writer.write(TAG_IGNORE); - writeAttribute(writer, ATTR_PATH, path); - writer.write(" />\n"); //$NON-NLS-1$ - } - writer.write(" </"); //$NON-NLS-1$ - writer.write(TAG_ISSUE); - writer.write('>'); - writer.write('\n'); - } else { - writer.write(" />\n"); //$NON-NLS-1$ - } - } - } - - writer.write("</lint>"); //$NON-NLS-1$ - writer.close(); - - // Move file into place: move current version to lint.xml~ (removing the old ~ file - // if it exists), then move the new version to lint.xml. - File oldFile = new File(mConfigFile.getParentFile(), - mConfigFile.getName() + '~'); //$NON-NLS-1$ - if (oldFile.exists()) { - oldFile.delete(); - } - if (mConfigFile.exists()) { - mConfigFile.renameTo(oldFile); - } - boolean ok = file.renameTo(mConfigFile); - if (ok && oldFile.exists()) { - oldFile.delete(); - } - } catch (Exception e) { - mClient.log(e, null); - } - } - - private static void writeAttribute( - @NonNull Writer writer, @NonNull String name, @NonNull String value) - throws IOException { - writer.write(' '); - writer.write(name); - writer.write('='); - writer.write('"'); - writer.write(value); - writer.write('"'); - } - - @Override - public void ignore( - @NonNull Context context, - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - // This configuration only supports suppressing warnings on a per-file basis - if (location != null) { - ignore(issue, location.getFile()); - } - } - - /** - * Marks the given issue and file combination as being ignored. - * - * @param issue the issue to be ignored in the given file - * @param file the file to ignore the issue in - */ - public void ignore(@NonNull Issue issue, @NonNull File file) { - ensureInitialized(); - - String path = mProject != null ? mProject.getRelativePath(file) : file.getPath(); - - List<String> paths = mSuppressed.get(issue.getId()); - if (paths == null) { - paths = new ArrayList<String>(); - mSuppressed.put(issue.getId(), paths); - } - paths.add(path); - - // Keep paths sorted alphabetically; makes XML output stable - Collections.sort(paths); - - if (!mBulkEditing) { - writeConfig(); - } - } - - @Override - public void setSeverity(@NonNull Issue issue, @Nullable Severity severity) { - ensureInitialized(); - - String id = issue.getId(); - if (severity == null) { - mSeverity.remove(id); - } else { - mSeverity.put(id, severity); - } - - if (!mBulkEditing) { - writeConfig(); - } - } - - @Override - public void startBulkEditing() { - mBulkEditing = true; - } - - @Override - public void finishBulkEditing() { - mBulkEditing = false; - writeConfig(); - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultSdkInfo.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultSdkInfo.java deleted file mode 100644 index 2f54659..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/DefaultSdkInfo.java +++ /dev/null @@ -1,288 +0,0 @@ -/* - * 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.client.api; - -import static com.android.SdkConstants.ABSOLUTE_LAYOUT; -import static com.android.SdkConstants.ABS_LIST_VIEW; -import static com.android.SdkConstants.ABS_SEEK_BAR; -import static com.android.SdkConstants.ABS_SPINNER; -import static com.android.SdkConstants.ADAPTER_VIEW; -import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; -import static com.android.SdkConstants.BUTTON; -import static com.android.SdkConstants.CHECKABLE; -import static com.android.SdkConstants.CHECKED_TEXT_VIEW; -import static com.android.SdkConstants.CHECK_BOX; -import static com.android.SdkConstants.COMPOUND_BUTTON; -import static com.android.SdkConstants.EDIT_TEXT; -import static com.android.SdkConstants.EXPANDABLE_LIST_VIEW; -import static com.android.SdkConstants.FRAME_LAYOUT; -import static com.android.SdkConstants.GALLERY; -import static com.android.SdkConstants.GRID_VIEW; -import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; -import static com.android.SdkConstants.IMAGE_BUTTON; -import static com.android.SdkConstants.IMAGE_VIEW; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.LIST_VIEW; -import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; -import static com.android.SdkConstants.PROGRESS_BAR; -import static com.android.SdkConstants.RADIO_BUTTON; -import static com.android.SdkConstants.RADIO_GROUP; -import static com.android.SdkConstants.RELATIVE_LAYOUT; -import static com.android.SdkConstants.SCROLL_VIEW; -import static com.android.SdkConstants.SEEK_BAR; -import static com.android.SdkConstants.SPINNER; -import static com.android.SdkConstants.SURFACE_VIEW; -import static com.android.SdkConstants.SWITCH; -import static com.android.SdkConstants.TABLE_LAYOUT; -import static com.android.SdkConstants.TABLE_ROW; -import static com.android.SdkConstants.TAB_HOST; -import static com.android.SdkConstants.TAB_WIDGET; -import static com.android.SdkConstants.TEXT_VIEW; -import static com.android.SdkConstants.TOGGLE_BUTTON; -import static com.android.SdkConstants.VIEW; -import static com.android.SdkConstants.VIEW_ANIMATOR; -import static com.android.SdkConstants.VIEW_GROUP; -import static com.android.SdkConstants.VIEW_PKG_PREFIX; -import static com.android.SdkConstants.VIEW_STUB; -import static com.android.SdkConstants.VIEW_SWITCHER; -import static com.android.SdkConstants.WEB_VIEW; -import static com.android.SdkConstants.WIDGET_PKG_PREFIX; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.annotations.Beta; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import java.util.HashMap; -import java.util.Map; -import java.util.Set; - -/** - * Default simple implementation of an {@link SdkInfo} - * <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 -class DefaultSdkInfo extends SdkInfo { - @Override - @Nullable - public String getParentViewName(@NonNull String name) { - name = getRawType(name); - - return PARENTS.get(name); - } - - @Override - @Nullable - public String getParentViewClass(@NonNull String fqcn) { - int index = fqcn.lastIndexOf('.'); - if (index != -1) { - fqcn = fqcn.substring(index + 1); - } - - String parent = PARENTS.get(fqcn); - if (parent == null) { - return null; - } - // The map only stores class names internally; correct for full package paths - if (parent.equals(VIEW) || parent.equals(VIEW_GROUP) || parent.equals(SURFACE_VIEW)) { - return VIEW_PKG_PREFIX + parent; - } else { - return WIDGET_PKG_PREFIX + parent; - } - } - - @Override - public boolean isSubViewOf(@NonNull String parentType, @NonNull String childType) { - String parent = getRawType(parentType); - String child = getRawType(childType); - - // Do analysis just on non-fqcn paths - if (parent.indexOf('.') != -1) { - parent = parent.substring(parent.lastIndexOf('.') + 1); - } - if (child.indexOf('.') != -1) { - child = child.substring(child.lastIndexOf('.') + 1); - } - - if (parent.equals(VIEW)) { - return true; - } - - while (!child.equals(VIEW)) { - if (parent.equals(child)) { - return true; - } - if (implementsInterface(child, parentType)) { - return true; - } - child = PARENTS.get(child); - if (child == null) { - // Unknown view - err on the side of caution - return true; - } - } - - return false; - } - - private static boolean implementsInterface(String className, String interfaceName) { - return interfaceName.equals(INTERFACES.get(className)); - } - - // Strip off type parameters, e.g. AdapterView<?> => AdapterView - private static String getRawType(String type) { - if (type != null) { - int index = type.indexOf('<'); - if (index != -1) { - type = type.substring(0, index); - } - } - - return type; - } - - @Override - public boolean isLayout(@NonNull String tag) { - // TODO: Read in widgets.txt from the platform install area to look up this information - // dynamically instead! - - if (super.isLayout(tag)) { - return true; - } - - return LAYOUTS.contains(tag); - } - - private static final int CLASS_COUNT = 59; - private static final int LAYOUT_COUNT = 20; - - private static final Map<String,String> PARENTS = Maps.newHashMapWithExpectedSize(CLASS_COUNT); - private static final Set<String> LAYOUTS = Sets.newHashSetWithExpectedSize(CLASS_COUNT); - - static { - PARENTS.put(COMPOUND_BUTTON, BUTTON); - PARENTS.put(ABS_SPINNER, ADAPTER_VIEW); - PARENTS.put(ABS_LIST_VIEW, ADAPTER_VIEW); - PARENTS.put(ABS_SEEK_BAR, ADAPTER_VIEW); - PARENTS.put(ADAPTER_VIEW, VIEW_GROUP); - PARENTS.put(VIEW_GROUP, VIEW); - - PARENTS.put(TEXT_VIEW, VIEW); - PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW); - PARENTS.put(RADIO_BUTTON, COMPOUND_BUTTON); - PARENTS.put(SPINNER, ABS_SPINNER); - PARENTS.put(IMAGE_BUTTON, IMAGE_VIEW); - PARENTS.put(IMAGE_VIEW, VIEW); - PARENTS.put(EDIT_TEXT, TEXT_VIEW); - PARENTS.put(PROGRESS_BAR, VIEW); - PARENTS.put(TOGGLE_BUTTON, COMPOUND_BUTTON); - PARENTS.put(VIEW_STUB, VIEW); - PARENTS.put(BUTTON, TEXT_VIEW); - PARENTS.put(SEEK_BAR, ABS_SEEK_BAR); - PARENTS.put(CHECK_BOX, COMPOUND_BUTTON); - PARENTS.put(SWITCH, COMPOUND_BUTTON); - PARENTS.put(GALLERY, ABS_SPINNER); - PARENTS.put(SURFACE_VIEW, VIEW); - PARENTS.put(ABSOLUTE_LAYOUT, VIEW_GROUP); - PARENTS.put(LINEAR_LAYOUT, VIEW_GROUP); - PARENTS.put(RELATIVE_LAYOUT, VIEW_GROUP); - PARENTS.put(LIST_VIEW, ABS_LIST_VIEW); - PARENTS.put(VIEW_SWITCHER, VIEW_ANIMATOR); - PARENTS.put(FRAME_LAYOUT, VIEW_GROUP); - PARENTS.put(HORIZONTAL_SCROLL_VIEW, FRAME_LAYOUT); - PARENTS.put(VIEW_ANIMATOR, FRAME_LAYOUT); - PARENTS.put(TAB_HOST, FRAME_LAYOUT); - PARENTS.put(TABLE_ROW, LINEAR_LAYOUT); - PARENTS.put(RADIO_GROUP, LINEAR_LAYOUT); - PARENTS.put(TAB_WIDGET, LINEAR_LAYOUT); - PARENTS.put(EXPANDABLE_LIST_VIEW, LIST_VIEW); - PARENTS.put(TABLE_LAYOUT, LINEAR_LAYOUT); - PARENTS.put(SCROLL_VIEW, FRAME_LAYOUT); - PARENTS.put(GRID_VIEW, ABS_LIST_VIEW); - PARENTS.put(WEB_VIEW, ABSOLUTE_LAYOUT); - PARENTS.put(AUTO_COMPLETE_TEXT_VIEW, EDIT_TEXT); - PARENTS.put(MULTI_AUTO_COMPLETE_TEXT_VIEW, AUTO_COMPLETE_TEXT_VIEW); - PARENTS.put(CHECKED_TEXT_VIEW, TEXT_VIEW); - - PARENTS.put("MediaController", FRAME_LAYOUT); //$NON-NLS-1$ - PARENTS.put("SlidingDrawer", VIEW_GROUP); //$NON-NLS-1$ - PARENTS.put("DialerFilter", RELATIVE_LAYOUT); //$NON-NLS-1$ - PARENTS.put("DigitalClock", TEXT_VIEW); //$NON-NLS-1$ - PARENTS.put("Chronometer", TEXT_VIEW); //$NON-NLS-1$ - PARENTS.put("ImageSwitcher", VIEW_SWITCHER); //$NON-NLS-1$ - PARENTS.put("TextSwitcher", VIEW_SWITCHER); //$NON-NLS-1$ - PARENTS.put("AnalogClock", VIEW); //$NON-NLS-1$ - PARENTS.put("TwoLineListItem", RELATIVE_LAYOUT); //$NON-NLS-1$ - PARENTS.put("ZoomControls", LINEAR_LAYOUT); //$NON-NLS-1$ - PARENTS.put("DatePicker", FRAME_LAYOUT); //$NON-NLS-1$ - PARENTS.put("TimePicker", FRAME_LAYOUT); //$NON-NLS-1$ - PARENTS.put("VideoView", SURFACE_VIEW); //$NON-NLS-1$ - PARENTS.put("ZoomButton", IMAGE_BUTTON); //$NON-NLS-1$ - PARENTS.put("RatingBar", ABS_SEEK_BAR); //$NON-NLS-1$ - PARENTS.put("ViewFlipper", VIEW_ANIMATOR); //$NON-NLS-1$ - PARENTS.put("NumberPicker", LINEAR_LAYOUT); //$NON-NLS-1$ - - assert PARENTS.size() <= CLASS_COUNT : PARENTS.size(); - - /* - // Check that all widgets lead to the root view - if (LintUtils.assertionsEnabled()) { - for (String key : PARENTS.keySet()) { - String parent = PARENTS.get(key); - if (!parent.equals(VIEW)) { - String grandParent = PARENTS.get(parent); - assert grandParent != null : parent; - } - } - } - */ - - LAYOUTS.add(TAB_HOST); - LAYOUTS.add(HORIZONTAL_SCROLL_VIEW); - LAYOUTS.add(VIEW_SWITCHER); - LAYOUTS.add(TAB_WIDGET); - LAYOUTS.add(VIEW_ANIMATOR); - LAYOUTS.add(SCROLL_VIEW); - LAYOUTS.add(GRID_VIEW); - LAYOUTS.add(TABLE_ROW); - LAYOUTS.add(RADIO_GROUP); - LAYOUTS.add(LIST_VIEW); - LAYOUTS.add(EXPANDABLE_LIST_VIEW); - LAYOUTS.add("MediaController"); //$NON-NLS-1$ - LAYOUTS.add("DialerFilter"); //$NON-NLS-1$ - LAYOUTS.add("ViewFlipper"); //$NON-NLS-1$ - LAYOUTS.add("SlidingDrawer"); //$NON-NLS-1$ - LAYOUTS.add("StackView"); //$NON-NLS-1$ - LAYOUTS.add("SearchView"); //$NON-NLS-1$ - LAYOUTS.add("TextSwitcher"); //$NON-NLS-1$ - LAYOUTS.add("AdapterViewFlipper"); //$NON-NLS-1$ - LAYOUTS.add("ImageSwitcher"); //$NON-NLS-1$ - assert LAYOUTS.size() <= LAYOUT_COUNT : LAYOUTS.size(); - } - - // Currently using a map; this should really be a list, but using a map until we actually - // start adding more than one item - @NonNull - private static final Map<String, String> INTERFACES = new HashMap<String, String>(2); - static { - INTERFACES.put(CHECKED_TEXT_VIEW, CHECKABLE); - INTERFACES.put(COMPOUND_BUTTON, CHECKABLE); - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IDomParser.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IDomParser.java deleted file mode 100644 index 1a70fac..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IDomParser.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.XmlContext; -import com.google.common.annotations.Beta; - -import org.w3c.dom.Document; -import org.w3c.dom.Node; - -/** - * A wrapper for an XML parser. This allows tools integrating lint to map directly - * to builtin services, such as already-parsed data structures in XML editors. - * <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 interface IDomParser { - /** - * Parse the file pointed to by the given context and return as a Document - * - * @param context the context pointing to the file to be parsed, typically - * via {@link Context#getContents()} but the file handle ( - * {@link Context#file} can also be used to map to an existing - * editor buffer in the surrounding tool, etc) - * @return the parsed DOM document, or null if parsing fails - */ - @Nullable - Document parseXml(@NonNull XmlContext context); - - /** - * Returns a {@link Location} for the given DOM node - * - * @param context information about the file being parsed - * @param node the node to create a location for - * @return a location for the given node - */ - @NonNull - Location getLocation(@NonNull XmlContext context, @NonNull Node node); - - /** - * Returns a {@link Location} for the given DOM node. Like - * {@link #getLocation(XmlContext, Node)}, but allows a position range that - * is a subset of the node range. - * - * @param context information about the file being parsed - * @param node the node to create a location for - * @param start the starting position within the node, inclusive - * @param end the ending position within the node, exclusive - * @return a location for the given node - */ - @NonNull - Location getLocation(@NonNull XmlContext context, @NonNull Node node, int start, int end); - - /** - * Creates a light-weight handle to a location for the given node. It can be - * turned into a full fledged location by - * {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}. - * - * @param context the context providing the node - * @param node the node (element or attribute) to create a location handle - * for - * @return a location handle - */ - @NonNull - Location.Handle createLocationHandle(@NonNull XmlContext context, @NonNull Node node); - - /** - * Dispose any data structures held for the given context. - * @param context information about the file previously parsed - * @param document the document that was parsed and is now being disposed - */ - void dispose(@NonNull XmlContext context, @NonNull Document document); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IJavaParser.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IJavaParser.java deleted file mode 100644 index 9b74f16..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IJavaParser.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Location; - -import lombok.ast.Node; - -/** - * A wrapper for a Java parser. This allows tools integrating lint to map directly - * to builtin services, such as already-parsed data structures in Java editors. - * <p/> - * <b>NOTE: This is not a or final API; if you rely on this be prepared - * to adjust your code for the next tools release.</b> - */ -public interface IJavaParser { - /** - * Parse the file pointed to by the given context. - * - * @param context the context pointing to the file to be parsed, typically - * via {@link Context#getContents()} but the file handle ( - * {@link Context#file} can also be used to map to an existing - * editor buffer in the surrounding tool, etc) - * @return the compilation unit node for the file - */ - @Nullable - Node parseJava(@NonNull JavaContext context); - - /** - * Returns a {@link Location} for the given node - * - * @param context information about the file being parsed - * @param node the node to create a location for - * @return a location for the given node - */ - @NonNull - Location getLocation(@NonNull JavaContext context, @NonNull Node node); - - /** - * Creates a light-weight handle to a location for the given node. It can be - * turned into a full fledged location by - * {@link com.android.tools.lint.detector.api.Location.Handle#resolve()}. - * - * @param context the context providing the node - * @param node the node (element or attribute) to create a location handle - * for - * @return a location handle - */ - @NonNull - Location.Handle createLocationHandle(@NonNull JavaContext context, @NonNull Node node); - - /** - * Dispose any data structures held for the given context. - * @param context information about the file previously parsed - * @param compilationUnit the compilation unit being disposed - */ - void dispose(@NonNull JavaContext context, @NonNull Node compilationUnit); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java deleted file mode 100644 index 0b8b141..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/IssueRegistry.java +++ /dev/null @@ -1,304 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.google.common.annotations.Beta; -import com.google.common.collect.Maps; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** Registry which provides a list of checks to be performed on an Android project - * <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 IssueRegistry { - private static List<Category> sCategories; - private static Map<String, Issue> sIdToIssue; - private static Map<EnumSet<Scope>, List<Issue>> sScopeIssues = Maps.newHashMap(); - - /** - * Creates a new {@linkplain IssueRegistry} - */ - protected IssueRegistry() { - } - - /** - * Issue reported by lint (not a specific detector) when it cannot even - * parse an XML file prior to analysis - */ - @NonNull - public static final Issue PARSER_ERROR = Issue.create( - "ParserError", //$NON-NLS-1$ - "Finds files that contain fatal parser errors", - "Lint will ignore any files that contain fatal parsing errors. These may contain " + - "other errors, or contain code which affects issues in other files.", - Category.CORRECTNESS, - 10, - Severity.ERROR, - Detector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** - * Issue reported by lint for various other issues which prevents lint from - * running normally when it's not necessarily an error in the user's code base. - */ - @NonNull - public static final Issue LINT_ERROR = Issue.create( - "LintError", //$NON-NLS-1$ - "Issues related to running lint itself, such as failure to read files, etc", - "This issue type represents a problem running lint itself. Examples include " + - "failure to find bytecode for source files (which means certain detectors " + - "could not be run), parsing errors in lint configuration files, etc." + - "\n" + - "These errors are not errors in your own code, but they are shown to make " + - "it clear that some checks were not completed.", - - Category.LINT, - 10, - Severity.ERROR, - Detector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** - * Returns the list of issues that can be found by all known detectors. - * - * @return the list of issues to be checked (including those that may be - * disabled!) - */ - @NonNull - public abstract List<Issue> getIssues(); - - /** - * Returns all available issues of a given scope (regardless of whether - * they are actually enabled for a given configuration etc) - * - * @param scope the applicable scope set - * @return a list of issues - */ - @NonNull - private List<Issue> getIssuesForScope(@NonNull EnumSet<Scope> scope) { - List<Issue> list = sScopeIssues.get(scope); - if (list == null) { - List<Issue> issues = getIssues(); - if (scope.equals(Scope.ALL)) { - list = issues; - } else { - int initialSize = 12; - if (scope.contains(Scope.RESOURCE_FILE)) { - initialSize += 50; - } - if (scope.contains(Scope.JAVA_FILE)) { - initialSize += 12; - } - if (scope.contains(Scope.CLASS_FILE)) { - initialSize += 12; - } - list = new ArrayList<Issue>(initialSize); - for (Issue issue : issues) { - // Determine if the scope matches - if (issue.isAdequate(scope)) { - list.add(issue); - } - } - } - sScopeIssues.put(scope, list); - } - - return list; - } - - /** - * Creates a list of detectors applicable to the given scope, and with the - * given configuration. - * - * @param client the client to report errors to - * @param configuration the configuration to look up which issues are - * enabled etc from - * @param scope the scope for the analysis, to filter out detectors that - * require wider analysis than is currently being performed - * @param scopeToDetectors an optional map which (if not null) will be - * filled by this method to contain mappings from each scope to - * the applicable detectors for that scope - * @return a list of new detector instances - */ - @NonNull - final List<? extends Detector> createDetectors( - @NonNull LintClient client, - @NonNull Configuration configuration, - @NonNull EnumSet<Scope> scope, - @Nullable Map<Scope, List<Detector>> scopeToDetectors) { - - List<Issue> issues = getIssuesForScope(scope); - if (issues.isEmpty()) { - return Collections.emptyList(); - } - - Set<Class<? extends Detector>> detectorClasses = new HashSet<Class<? extends Detector>>(); - Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope = - new HashMap<Class<? extends Detector>, EnumSet<Scope>>(); - - for (Issue issue : issues) { - Class<? extends Detector> detectorClass = issue.getDetectorClass(); - EnumSet<Scope> issueScope = issue.getScope(); - if (!detectorClasses.contains(detectorClass)) { - // Determine if the issue is enabled - if (!configuration.isEnabled(issue)) { - continue; - } - - assert issue.isAdequate(scope); // Ensured by getIssuesForScope above - - detectorClass = client.replaceDetector(detectorClass); - - assert detectorClass != null : issue.getId(); - detectorClasses.add(detectorClass); - } - - if (scopeToDetectors != null) { - EnumSet<Scope> s = detectorToScope.get(detectorClass); - if (s == null) { - detectorToScope.put(detectorClass, issueScope); - } else if (!s.containsAll(issueScope)) { - EnumSet<Scope> union = EnumSet.copyOf(s); - union.addAll(issueScope); - detectorToScope.put(detectorClass, union); - } - } - } - - List<Detector> detectors = new ArrayList<Detector>(detectorClasses.size()); - for (Class<? extends Detector> clz : detectorClasses) { - try { - Detector detector = clz.newInstance(); - detectors.add(detector); - - if (scopeToDetectors != null) { - EnumSet<Scope> union = detectorToScope.get(clz); - for (Scope s : union) { - List<Detector> list = scopeToDetectors.get(s); - if (list == null) { - list = new ArrayList<Detector>(); - scopeToDetectors.put(s, list); - } - list.add(detector); - } - - } - } catch (Throwable t) { - client.log(t, "Can't initialize detector %1$s", clz.getName()); //$NON-NLS-1$ - } - } - - return detectors; - } - - /** - * Returns true if the given id represents a valid issue id - * - * @param id the id to be checked - * @return true if the given id is valid - */ - public final boolean isIssueId(@NonNull String id) { - return getIssue(id) != null; - } - - /** - * Returns true if the given category is a valid category - * - * @param name the category name to be checked - * @return true if the given string is a valid category - */ - public final boolean isCategoryName(@NonNull String name) { - for (Category category : getCategories()) { - if (category.getName().equals(name) || category.getFullName().equals(name)) { - return true; - } - } - - return false; - } - - /** - * Returns the available categories - * - * @return an iterator for all the categories, never null - */ - @NonNull - public List<Category> getCategories() { - if (sCategories == null) { - final Set<Category> categories = new HashSet<Category>(); - for (Issue issue : getIssues()) { - categories.add(issue.getCategory()); - } - List<Category> sorted = new ArrayList<Category>(categories); - Collections.sort(sorted); - sCategories = Collections.unmodifiableList(sorted); - } - - return sCategories; - } - - /** - * Returns the issue for the given id, or null if it's not a valid id - * - * @param id the id to be checked - * @return the corresponding issue, or null - */ - @Nullable - public final Issue getIssue(@NonNull String id) { - if (sIdToIssue == null) { - List<Issue> issues = getIssues(); - sIdToIssue = new HashMap<String, Issue>(issues.size()); - for (Issue issue : issues) { - sIdToIssue.put(issue.getId(), issue); - } - - sIdToIssue.put(PARSER_ERROR.getId(), PARSER_ERROR); - sIdToIssue.put(LINT_ERROR.getId(), LINT_ERROR); - } - return sIdToIssue.get(id); - } - - /** - * Reset the registry such that it recomputes its available issues. - * <p> - * NOTE: This is only intended for testing purposes. - */ - @VisibleForTesting - protected static void reset() { - sIdToIssue = null; - sCategories = null; - sScopeIssues = Maps.newHashMap(); - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java deleted file mode 100644 index 81a0339..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/JavaVisitor.java +++ /dev/null @@ -1,1195 +0,0 @@ -/* - * 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.client.api; - -import static com.android.SdkConstants.ANDROID_PKG; -import static com.android.SdkConstants.R_CLASS; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.JavaScanner; -import com.android.tools.lint.detector.api.Detector.XmlScanner; -import com.android.tools.lint.detector.api.JavaContext; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import lombok.ast.AlternateConstructorInvocation; -import lombok.ast.Annotation; -import lombok.ast.AnnotationDeclaration; -import lombok.ast.AnnotationElement; -import lombok.ast.AnnotationMethodDeclaration; -import lombok.ast.AnnotationValueArray; -import lombok.ast.ArrayAccess; -import lombok.ast.ArrayCreation; -import lombok.ast.ArrayDimension; -import lombok.ast.ArrayInitializer; -import lombok.ast.Assert; -import lombok.ast.AstVisitor; -import lombok.ast.BinaryExpression; -import lombok.ast.Block; -import lombok.ast.BooleanLiteral; -import lombok.ast.Break; -import lombok.ast.Case; -import lombok.ast.Cast; -import lombok.ast.Catch; -import lombok.ast.CharLiteral; -import lombok.ast.ClassDeclaration; -import lombok.ast.ClassLiteral; -import lombok.ast.Comment; -import lombok.ast.CompilationUnit; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.ConstructorInvocation; -import lombok.ast.Continue; -import lombok.ast.Default; -import lombok.ast.DoWhile; -import lombok.ast.EmptyDeclaration; -import lombok.ast.EmptyStatement; -import lombok.ast.EnumConstant; -import lombok.ast.EnumDeclaration; -import lombok.ast.EnumTypeBody; -import lombok.ast.Expression; -import lombok.ast.ExpressionStatement; -import lombok.ast.FloatingPointLiteral; -import lombok.ast.For; -import lombok.ast.ForEach; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.Identifier; -import lombok.ast.If; -import lombok.ast.ImportDeclaration; -import lombok.ast.InlineIfExpression; -import lombok.ast.InstanceInitializer; -import lombok.ast.InstanceOf; -import lombok.ast.IntegralLiteral; -import lombok.ast.InterfaceDeclaration; -import lombok.ast.KeywordModifier; -import lombok.ast.LabelledStatement; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.Modifiers; -import lombok.ast.Node; -import lombok.ast.NormalTypeBody; -import lombok.ast.NullLiteral; -import lombok.ast.PackageDeclaration; -import lombok.ast.Return; -import lombok.ast.Select; -import lombok.ast.StaticInitializer; -import lombok.ast.StringLiteral; -import lombok.ast.Super; -import lombok.ast.SuperConstructorInvocation; -import lombok.ast.Switch; -import lombok.ast.Synchronized; -import lombok.ast.This; -import lombok.ast.Throw; -import lombok.ast.Try; -import lombok.ast.TypeReference; -import lombok.ast.TypeReferencePart; -import lombok.ast.TypeVariable; -import lombok.ast.UnaryExpression; -import lombok.ast.VariableDeclaration; -import lombok.ast.VariableDefinition; -import lombok.ast.VariableDefinitionEntry; -import lombok.ast.VariableReference; -import lombok.ast.While; - - -/** - * Specialized visitor for running detectors on a Java AST. - * It operates in three phases: - * <ol> - * <li> First, it computes a set of maps where it generates a map from each - * significant AST attribute (such as method call names) to a list - * of detectors to consult whenever that attribute is encountered. - * Examples of "attributes" are method names, Android resource identifiers, - * and general AST node types such as "cast" nodes etc. These are - * defined on the {@link JavaScanner} interface. - * <li> Second, it iterates over the document a single time, delegating to - * the detectors found at each relevant AST attribute. - * <li> Finally, it calls the remaining visitors (those that need to process a - * whole document on their own). - * </ol> - * It also notifies all the detectors before and after the document is processed - * such that they can do pre- and post-processing. - */ -public class JavaVisitor { - /** Default size of lists holding detectors of the same type for a given node type */ - private static final int SAME_TYPE_COUNT = 8; - - private final Map<String, List<VisitingDetector>> mMethodDetectors = - new HashMap<String, List<VisitingDetector>>(); - private final List<VisitingDetector> mResourceFieldDetectors = - new ArrayList<VisitingDetector>(); - private final List<VisitingDetector> mAllDetectors; - private final List<VisitingDetector> mFullTreeDetectors; - private final Map<Class<? extends Node>, List<VisitingDetector>> mNodeTypeDetectors = - new HashMap<Class<? extends Node>, List<VisitingDetector>>(); - private final IJavaParser mParser; - - JavaVisitor(@NonNull IJavaParser parser, @NonNull List<Detector> detectors) { - mParser = parser; - mAllDetectors = new ArrayList<VisitingDetector>(detectors.size()); - mFullTreeDetectors = new ArrayList<VisitingDetector>(detectors.size()); - - for (Detector detector : detectors) { - VisitingDetector v = new VisitingDetector(detector, (JavaScanner) detector); - mAllDetectors.add(v); - - List<Class<? extends Node>> nodeTypes = detector.getApplicableNodeTypes(); - if (nodeTypes != null) { - for (Class<? extends Node> type : nodeTypes) { - List<VisitingDetector> list = mNodeTypeDetectors.get(type); - if (list == null) { - list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT); - mNodeTypeDetectors.put(type, list); - } - list.add(v); - } - } - - List<String> names = detector.getApplicableMethodNames(); - if (names != null) { - // not supported in Java visitors; adding a method invocation node is trivial - // for that case. - assert names != XmlScanner.ALL; - - for (String name : names) { - List<VisitingDetector> list = mMethodDetectors.get(name); - if (list == null) { - list = new ArrayList<VisitingDetector>(SAME_TYPE_COUNT); - mMethodDetectors.put(name, list); - } - list.add(v); - } - } - - if (detector.appliesToResourceRefs()) { - mResourceFieldDetectors.add(v); - } else if ((names == null || names.isEmpty()) - && (nodeTypes == null || nodeTypes.isEmpty())) { - mFullTreeDetectors.add(v); - } - } - } - - void visitFile(@NonNull JavaContext context, @NonNull File file) { - context.parser = mParser; - - Node compilationUnit = null; - try { - compilationUnit = mParser.parseJava(context); - if (compilationUnit == null) { - // No need to log this; the parser should be reporting - // a full warning (such as IssueRegistry#PARSER_ERROR) - // with details, location, etc. - return; - } - context.compilationUnit = compilationUnit; - - for (VisitingDetector v : mAllDetectors) { - v.setContext(context); - v.getDetector().beforeCheckFile(context); - } - - for (VisitingDetector v : mFullTreeDetectors) { - AstVisitor visitor = v.getVisitor(); - if (visitor != null) { - compilationUnit.accept(visitor); - } - } - - if (!mMethodDetectors.isEmpty() || !mResourceFieldDetectors.isEmpty()) { - AstVisitor visitor = new DelegatingJavaVisitor(context); - compilationUnit.accept(visitor); - } else if (!mNodeTypeDetectors.isEmpty()) { - AstVisitor visitor = new DispatchVisitor(); - compilationUnit.accept(visitor); - } - - for (VisitingDetector v : mAllDetectors) { - v.getDetector().afterCheckFile(context); - } - } finally { - if (compilationUnit != null) { - mParser.dispose(context, compilationUnit); - } - } - } - - private static class VisitingDetector { - private AstVisitor mVisitor; // construct lazily, and clear out on context switch! - private JavaContext mContext; - public final Detector mDetector; - public final JavaScanner mJavaScanner; - - public VisitingDetector(@NonNull Detector detector, @NonNull JavaScanner javaScanner) { - mDetector = detector; - mJavaScanner = javaScanner; - } - - @NonNull - public Detector getDetector() { - return mDetector; - } - - @NonNull - public JavaScanner getJavaScanner() { - return mJavaScanner; - } - - public void setContext(@NonNull JavaContext context) { - mContext = context; - - // The visitors are one-per-context, so clear them out here and construct - // lazily only if needed - mVisitor = null; - } - - @NonNull - AstVisitor getVisitor() { - if (mVisitor == null) { - mVisitor = mDetector.createJavaVisitor(mContext); - if (mVisitor == null) { - mVisitor = new ForwardingAstVisitor() { - }; - } - } - return mVisitor; - } - } - - /** - * Generic dispatcher which visits all nodes (once) and dispatches to - * specific visitors for each node. Each visitor typically only wants to - * look at a small part of a tree, such as a method call or a class - * declaration, so this means we avoid visiting all "uninteresting" nodes in - * the tree repeatedly. - */ - private class DispatchVisitor extends AstVisitor { - @Override - public void endVisit(Node node) { - for (VisitingDetector v : mAllDetectors) { - v.getVisitor().endVisit(node); - } - } - - @Override - public boolean visitAlternateConstructorInvocation(AlternateConstructorInvocation node) { - List<VisitingDetector> list = - mNodeTypeDetectors.get(AlternateConstructorInvocation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAlternateConstructorInvocation(node); - } - } - return false; - } - - @Override - public boolean visitAnnotation(Annotation node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Annotation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAnnotation(node); - } - } - return false; - } - - @Override - public boolean visitAnnotationDeclaration(AnnotationDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAnnotationDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitAnnotationElement(AnnotationElement node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationElement.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAnnotationElement(node); - } - } - return false; - } - - @Override - public boolean visitAnnotationMethodDeclaration(AnnotationMethodDeclaration node) { - List<VisitingDetector> list = - mNodeTypeDetectors.get(AnnotationMethodDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAnnotationMethodDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitAnnotationValueArray(AnnotationValueArray node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(AnnotationValueArray.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAnnotationValueArray(node); - } - } - return false; - } - - @Override - public boolean visitArrayAccess(ArrayAccess node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayAccess.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitArrayAccess(node); - } - } - return false; - } - - @Override - public boolean visitArrayCreation(ArrayCreation node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayCreation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitArrayCreation(node); - } - } - return false; - } - - @Override - public boolean visitArrayDimension(ArrayDimension node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayDimension.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitArrayDimension(node); - } - } - return false; - } - - @Override - public boolean visitArrayInitializer(ArrayInitializer node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ArrayInitializer.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitArrayInitializer(node); - } - } - return false; - } - - @Override - public boolean visitAssert(Assert node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Assert.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitAssert(node); - } - } - return false; - } - - @Override - public boolean visitBinaryExpression(BinaryExpression node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(BinaryExpression.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitBinaryExpression(node); - } - } - return false; - } - - @Override - public boolean visitBlock(Block node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Block.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitBlock(node); - } - } - return false; - } - - @Override - public boolean visitBooleanLiteral(BooleanLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(BooleanLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitBooleanLiteral(node); - } - } - return false; - } - - @Override - public boolean visitBreak(Break node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Break.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitBreak(node); - } - } - return false; - } - - @Override - public boolean visitCase(Case node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Case.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitCase(node); - } - } - return false; - } - - @Override - public boolean visitCast(Cast node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Cast.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitCast(node); - } - } - return false; - } - - @Override - public boolean visitCatch(Catch node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Catch.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitCatch(node); - } - } - return false; - } - - @Override - public boolean visitCharLiteral(CharLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(CharLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitCharLiteral(node); - } - } - return false; - } - - @Override - public boolean visitClassDeclaration(ClassDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ClassDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitClassDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitClassLiteral(ClassLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ClassLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitClassLiteral(node); - } - } - return false; - } - - @Override - public boolean visitComment(Comment node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Comment.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitComment(node); - } - } - return false; - } - - @Override - public boolean visitCompilationUnit(CompilationUnit node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(CompilationUnit.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitCompilationUnit(node); - } - } - return false; - } - - @Override - public boolean visitConstructorDeclaration(ConstructorDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ConstructorDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitConstructorDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitConstructorInvocation(ConstructorInvocation node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ConstructorInvocation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitConstructorInvocation(node); - } - } - return false; - } - - @Override - public boolean visitContinue(Continue node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Continue.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitContinue(node); - } - } - return false; - } - - @Override - public boolean visitDefault(Default node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Default.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitDefault(node); - } - } - return false; - } - - @Override - public boolean visitDoWhile(DoWhile node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(DoWhile.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitDoWhile(node); - } - } - return false; - } - - @Override - public boolean visitEmptyDeclaration(EmptyDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(EmptyDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitEmptyDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitEmptyStatement(EmptyStatement node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(EmptyStatement.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitEmptyStatement(node); - } - } - return false; - } - - @Override - public boolean visitEnumConstant(EnumConstant node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(EnumConstant.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitEnumConstant(node); - } - } - return false; - } - - @Override - public boolean visitEnumDeclaration(EnumDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(EnumDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitEnumDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitEnumTypeBody(EnumTypeBody node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(EnumTypeBody.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitEnumTypeBody(node); - } - } - return false; - } - - @Override - public boolean visitExpressionStatement(ExpressionStatement node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ExpressionStatement.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitExpressionStatement(node); - } - } - return false; - } - - @Override - public boolean visitFloatingPointLiteral(FloatingPointLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(FloatingPointLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitFloatingPointLiteral(node); - } - } - return false; - } - - @Override - public boolean visitFor(For node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(For.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitFor(node); - } - } - return false; - } - - @Override - public boolean visitForEach(ForEach node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ForEach.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitForEach(node); - } - } - return false; - } - - @Override - public boolean visitIdentifier(Identifier node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Identifier.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitIdentifier(node); - } - } - return false; - } - - @Override - public boolean visitIf(If node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(If.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitIf(node); - } - } - return false; - } - - @Override - public boolean visitImportDeclaration(ImportDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(ImportDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitImportDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitInlineIfExpression(InlineIfExpression node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(InlineIfExpression.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitInlineIfExpression(node); - } - } - return false; - } - - @Override - public boolean visitInstanceInitializer(InstanceInitializer node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(InstanceInitializer.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitInstanceInitializer(node); - } - } - return false; - } - - @Override - public boolean visitInstanceOf(InstanceOf node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(InstanceOf.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitInstanceOf(node); - } - } - return false; - } - - @Override - public boolean visitIntegralLiteral(IntegralLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(IntegralLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitIntegralLiteral(node); - } - } - return false; - } - - @Override - public boolean visitInterfaceDeclaration(InterfaceDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(InterfaceDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitInterfaceDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitKeywordModifier(KeywordModifier node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(KeywordModifier.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitKeywordModifier(node); - } - } - return false; - } - - @Override - public boolean visitLabelledStatement(LabelledStatement node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(LabelledStatement.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitLabelledStatement(node); - } - } - return false; - } - - @Override - public boolean visitMethodDeclaration(MethodDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(MethodDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitMethodDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(MethodInvocation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitMethodInvocation(node); - } - } - return false; - } - - @Override - public boolean visitModifiers(Modifiers node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Modifiers.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitModifiers(node); - } - } - return false; - } - - @Override - public boolean visitNormalTypeBody(NormalTypeBody node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(NormalTypeBody.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitNormalTypeBody(node); - } - } - return false; - } - - @Override - public boolean visitNullLiteral(NullLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(NullLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitNullLiteral(node); - } - } - return false; - } - - @Override - public boolean visitPackageDeclaration(PackageDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(PackageDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitPackageDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitParseArtefact(Node node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Node.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitParseArtefact(node); - } - } - return false; - } - - @Override - public boolean visitReturn(Return node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Return.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitReturn(node); - } - } - return false; - } - - @Override - public boolean visitSelect(Select node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Select.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitSelect(node); - } - } - return false; - } - - @Override - public boolean visitStaticInitializer(StaticInitializer node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(StaticInitializer.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitStaticInitializer(node); - } - } - return false; - } - - @Override - public boolean visitStringLiteral(StringLiteral node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(StringLiteral.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitStringLiteral(node); - } - } - return false; - } - - @Override - public boolean visitSuper(Super node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Super.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitSuper(node); - } - } - return false; - } - - @Override - public boolean visitSuperConstructorInvocation(SuperConstructorInvocation node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(SuperConstructorInvocation.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitSuperConstructorInvocation(node); - } - } - return false; - } - - @Override - public boolean visitSwitch(Switch node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Switch.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitSwitch(node); - } - } - return false; - } - - @Override - public boolean visitSynchronized(Synchronized node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Synchronized.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitSynchronized(node); - } - } - return false; - } - - @Override - public boolean visitThis(This node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(This.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitThis(node); - } - } - return false; - } - - @Override - public boolean visitThrow(Throw node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Throw.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitThrow(node); - } - } - return false; - } - - @Override - public boolean visitTry(Try node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(Try.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitTry(node); - } - } - return false; - } - - @Override - public boolean visitTypeReference(TypeReference node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(TypeReference.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitTypeReference(node); - } - } - return false; - } - - @Override - public boolean visitTypeReferencePart(TypeReferencePart node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(TypeReferencePart.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitTypeReferencePart(node); - } - } - return false; - } - - @Override - public boolean visitTypeVariable(TypeVariable node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(TypeVariable.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitTypeVariable(node); - } - } - return false; - } - - @Override - public boolean visitUnaryExpression(UnaryExpression node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(UnaryExpression.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitUnaryExpression(node); - } - } - return false; - } - - @Override - public boolean visitVariableDeclaration(VariableDeclaration node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDeclaration.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitVariableDeclaration(node); - } - } - return false; - } - - @Override - public boolean visitVariableDefinition(VariableDefinition node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDefinition.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitVariableDefinition(node); - } - } - return false; - } - - @Override - public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(VariableDefinitionEntry.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitVariableDefinitionEntry(node); - } - } - return false; - } - - @Override - public boolean visitVariableReference(VariableReference node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(VariableReference.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitVariableReference(node); - } - } - return false; - } - - @Override - public boolean visitWhile(While node) { - List<VisitingDetector> list = mNodeTypeDetectors.get(While.class); - if (list != null) { - for (VisitingDetector v : list) { - v.getVisitor().visitWhile(node); - } - } - return false; - } - } - - /** Performs common AST searches for method calls and R-type-field references. - * Note that this is a specialized form of the {@link DispatchVisitor}. */ - private class DelegatingJavaVisitor extends DispatchVisitor { - private final JavaContext mContext; - private final boolean mVisitResources; - private final boolean mVisitMethods; - - public DelegatingJavaVisitor(JavaContext context) { - mContext = context; - - mVisitMethods = !mMethodDetectors.isEmpty(); - mVisitResources = !mResourceFieldDetectors.isEmpty(); - } - - @Override - public boolean visitSelect(Select node) { - if (mVisitResources) { - // R.type.name - if (node.astOperand() instanceof Select) { - Select select = (Select) node.astOperand(); - if (select.astOperand() instanceof VariableReference) { - VariableReference reference = (VariableReference) select.astOperand(); - if (reference.astIdentifier().astValue().equals(R_CLASS)) { - String type = select.astIdentifier().astValue(); - String name = node.astIdentifier().astValue(); - - // R -could- be referenced locally and really have been - // imported as "import android.R;" in the import statements, - // but this is not recommended (and in fact there's a specific - // lint rule warning against it) - boolean isFramework = false; - - for (VisitingDetector v : mResourceFieldDetectors) { - JavaScanner detector = v.getJavaScanner(); - detector.visitResourceReference(mContext, v.getVisitor(), - node, type, name, isFramework); - } - - return super.visitSelect(node); - } - } - } - - // Arbitrary packages -- android.R.type.name, foo.bar.R.type.name - if (node.astIdentifier().astValue().equals(R_CLASS)) { - Node parent = node.getParent(); - if (parent instanceof Select) { - Node grandParent = parent.getParent(); - if (grandParent instanceof Select) { - Select select = (Select) grandParent; - String name = select.astIdentifier().astValue(); - Expression typeOperand = select.astOperand(); - if (typeOperand instanceof Select) { - Select typeSelect = (Select) typeOperand; - String type = typeSelect.astIdentifier().astValue(); - boolean isFramework = - node.astIdentifier().astValue().equals(ANDROID_PKG); - for (VisitingDetector v : mResourceFieldDetectors) { - JavaScanner detector = v.getJavaScanner(); - detector.visitResourceReference(mContext, v.getVisitor(), - node, type, name, isFramework); - } - } - } - } - } - } - - return super.visitSelect(node); - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (mVisitMethods) { - String methodName = node.astName().getDescription(); - List<VisitingDetector> list = mMethodDetectors.get(methodName); - if (list != null) { - for (VisitingDetector v : list) { - v.getJavaScanner().visitMethod(mContext, v.getVisitor(), node); - } - } - } - - return super.visitMethodInvocation(node); - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintClient.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintClient.java deleted file mode 100644 index b8118b7..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintClient.java +++ /dev/null @@ -1,691 +0,0 @@ -/* - * 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.client.api; - -import static com.android.SdkConstants.CLASS_FOLDER; -import static com.android.SdkConstants.DOT_JAR; -import static com.android.SdkConstants.GEN_FOLDER; -import static com.android.SdkConstants.LIBS_FOLDER; -import static com.android.SdkConstants.RES_FOLDER; -import static com.android.SdkConstants.SRC_FOLDER; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.sdklib.IAndroidTarget; -import com.android.sdklib.SdkManager; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -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.Project; -import com.android.tools.lint.detector.api.Severity; -import com.android.utils.StdLogger; -import com.android.utils.StdLogger.Level; -import com.google.common.annotations.Beta; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.io.Files; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; - -import java.io.File; -import java.io.IOException; -import java.io.StringReader; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; - -/** - * Information about the tool embedding the lint analyzer. IDEs and other tools - * implementing lint support will extend this to integrate logging, displaying errors, - * etc. - * <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 LintClient { - private static final String PROP_BIN_DIR = "com.android.tools.lint.bindir"; //$NON-NLS-1$ - - /** - * Returns a configuration for use by the given project. The configuration - * provides information about which issues are enabled, any customizations - * to the severity of an issue, etc. - * <p> - * By default this method returns a {@link DefaultConfiguration}. - * - * @param project the project to obtain a configuration for - * @return a configuration, never null. - */ - public Configuration getConfiguration(@NonNull Project project) { - return DefaultConfiguration.create(this, project, null); - } - - /** - * Report the given issue. This method will only be called if the configuration - * provided by {@link #getConfiguration(Project)} has reported the corresponding - * issue as enabled and has not filtered out the issue with its - * {@link Configuration#ignore(Context, Issue, Location, String, Object)} method. - * <p> - * - * @param context the context used by the detector when the issue was found - * @param issue the issue that was found - * @param severity the severity of the issue - * @param location the location of the issue - * @param message the associated user message - * @param data optional extra data for a discovered issue, or null. The - * content depends on the specific issue. Detectors can pass - * extra info here which automatic fix tools etc can use to - * extract relevant information instead of relying on parsing the - * error message text. See each detector for details on which - * data if any is supplied for a given issue. - */ - public abstract void report( - @NonNull Context context, - @NonNull Issue issue, - @NonNull Severity severity, - @Nullable Location location, - @NonNull String message, - @Nullable Object data); - - /** - * Send an exception or error message (with warning severity) to the log - * - * @param exception the exception, possibly null - * @param format the error message using {@link String#format} syntax, possibly null - * (though in that case the exception should not be null) - * @param args any arguments for the format string - */ - public void log( - @Nullable Throwable exception, - @Nullable String format, - @Nullable Object... args) { - log(Severity.WARNING, exception, format, args); - } - - /** - * Send an exception or error message to the log - * - * @param severity the severity of the warning - * @param exception the exception, possibly null - * @param format the error message using {@link String#format} syntax, possibly null - * (though in that case the exception should not be null) - * @param args any arguments for the format string - */ - public abstract void log( - @NonNull Severity severity, - @Nullable Throwable exception, - @Nullable String format, - @Nullable Object... args); - - /** - * Returns a {@link IDomParser} to use to parse XML - * - * @return a new {@link IDomParser}, or null if this client does not support - * XML analysis - */ - @Nullable - public abstract IDomParser getDomParser(); - - /** - * Returns a {@link IJavaParser} to use to parse Java - * - * @return a new {@link IJavaParser}, or null if this client does not - * support Java analysis - */ - @Nullable - public abstract IJavaParser getJavaParser(); - - /** - * Returns an optimal detector, if applicable. By default, just returns the - * original detector, but tools can replace detectors using this hook with a version - * that takes advantage of native capabilities of the tool. - * - * @param detectorClass the class of the detector to be replaced - * @return the new detector class, or just the original detector (not null) - */ - @NonNull - public Class<? extends Detector> replaceDetector( - @NonNull Class<? extends Detector> detectorClass) { - return detectorClass; - } - - /** - * Reads the given text file and returns the content as a string - * - * @param file the file to read - * @return the string to return, never null (will be empty if there is an - * I/O error) - */ - @NonNull - public abstract String readFile(@NonNull File file); - - /** - * Reads the given binary file and returns the content as a byte array. - * By default this method will read the bytes from the file directly, - * but this can be customized by a client if for example I/O could be - * held in memory and not flushed to disk yet. - * - * @param file the file to read - * @return the bytes in the file, never null - * @throws IOException if the file does not exist, or if the file cannot be - * read for some reason - */ - @NonNull - public byte[] readBytes(@NonNull File file) throws IOException { - return Files.toByteArray(file); - } - - /** - * Returns the list of source folders for Java source files - * - * @param project the project to look up Java source file locations for - * @return a list of source folders to search for .java files - */ - @NonNull - public List<File> getJavaSourceFolders(@NonNull Project project) { - return getClassPath(project).getSourceFolders(); - } - - /** - * Returns the list of output folders for class files - * - * @param project the project to look up class file locations for - * @return a list of output folders to search for .class files - */ - @NonNull - public List<File> getJavaClassFolders(@NonNull Project project) { - return getClassPath(project).getClassFolders(); - - } - - /** - * Returns the list of Java libraries - * - * @param project the project to look up jar dependencies for - * @return a list of jar dependencies containing .class files - */ - @NonNull - public List<File> getJavaLibraries(@NonNull Project project) { - return getClassPath(project).getLibraries(); - } - - /** - * Returns the resource folders. - * - * @param project the project to look up the resource folder for - * @return a list of files pointing to the resource folders, possibly empty - */ - @NonNull - public List<File> getResourceFolders(@NonNull Project project) { - File res = new File(project.getDir(), RES_FOLDER); - if (res.exists()) { - return Collections.singletonList(res); - } - - return Collections.emptyList(); - } - - /** - * Returns the {@link SdkInfo} to use for the given project. - * - * @param project the project to look up an {@link SdkInfo} for - * @return an {@link SdkInfo} for the project - */ - @NonNull - public SdkInfo getSdkInfo(@NonNull Project project) { - // By default no per-platform SDK info - return new DefaultSdkInfo(); - } - - /** - * Returns a suitable location for storing cache files. Note that the - * directory may not exist. - * - * @param create if true, attempt to create the cache dir if it does not - * exist - * @return a suitable location for storing cache files, which may be null if - * the create flag was false, or if for some reason the directory - * could not be created - */ - @Nullable - public File getCacheDir(boolean create) { - String home = System.getProperty("user.home"); - String relative = ".android" + File.separator + "cache"; //$NON-NLS-1$ //$NON-NLS-2$ - File dir = new File(home, relative); - if (create && !dir.exists()) { - if (!dir.mkdirs()) { - return null; - } - } - return dir; - } - - /** - * Returns the File corresponding to the system property or the environment variable - * for {@link #PROP_BIN_DIR}. - * This property is typically set by the SDK/tools/lint[.bat] wrapper. - * It denotes the path of the wrapper on disk. - * - * @return A new File corresponding to {@link LintClient#PROP_BIN_DIR} or null. - */ - @Nullable - private static File getLintBinDir() { - // First check the Java properties (e.g. set using "java -jar ... -Dname=value") - String path = System.getProperty(PROP_BIN_DIR); - if (path == null || path.isEmpty()) { - // If not found, check environment variables. - path = System.getenv(PROP_BIN_DIR); - } - if (path != null && !path.isEmpty()) { - return new File(path); - } - return null; - } - - /** - * Returns the File pointing to the user's SDK install area. This is generally - * the root directory containing the lint tool (but also platforms/ etc). - * - * @return a file pointing to the user's install area - */ - @Nullable - public File getSdkHome() { - File binDir = getLintBinDir(); - if (binDir != null) { - assert binDir.getName().equals("tools"); - - File root = binDir.getParentFile(); - if (root != null && root.isDirectory()) { - return root; - } - } - - String home = System.getenv("ANDROID_HOME"); //$NON-NLS-1$ - if (home != null) { - return new File(home); - } - - return null; - } - - /** - * Locates an SDK resource (relative to the SDK root directory). - * <p> - * TODO: Consider switching to a {@link URL} return type instead. - * - * @param relativePath A relative path (using {@link File#separator} to - * separate path components) to the given resource - * @return a {@link File} pointing to the resource, or null if it does not - * exist - */ - @Nullable - public File findResource(@NonNull String relativePath) { - File top = getSdkHome(); - if (top == null) { - throw new IllegalArgumentException("Lint must be invoked with the System property " - + PROP_BIN_DIR + " pointing to the ANDROID_SDK tools directory"); - } - - File file = new File(top, relativePath); - if (file.exists()) { - return file; - } else { - return null; - } - } - - private Map<Project, ClassPathInfo> mProjectInfo; - - /** - * Information about class paths (sources, class files and libraries) - * usually associated with a project. - */ - protected static class ClassPathInfo { - private final List<File> mClassFolders; - private final List<File> mSourceFolders; - private final List<File> mLibraries; - - public ClassPathInfo( - @NonNull List<File> sourceFolders, - @NonNull List<File> classFolders, - @NonNull List<File> libraries) { - mSourceFolders = sourceFolders; - mClassFolders = classFolders; - mLibraries = libraries; - } - - @NonNull - public List<File> getSourceFolders() { - return mSourceFolders; - } - - @NonNull - public List<File> getClassFolders() { - return mClassFolders; - } - - @NonNull - public List<File> getLibraries() { - return mLibraries; - } - } - - /** - * Considers the given project as an Eclipse project and returns class path - * information for the project - the source folder(s), the output folder and - * any libraries. - * <p> - * Callers will not cache calls to this method, so if it's expensive to compute - * the classpath info, this method should perform its own caching. - * - * @param project the project to look up class path info for - * @return a class path info object, never null - */ - @NonNull - protected ClassPathInfo getClassPath(@NonNull Project project) { - ClassPathInfo info; - if (mProjectInfo == null) { - mProjectInfo = Maps.newHashMap(); - info = null; - } else { - info = mProjectInfo.get(project); - } - - if (info == null) { - List<File> sources = new ArrayList<File>(2); - List<File> classes = new ArrayList<File>(1); - List<File> libraries = new ArrayList<File>(); - - File projectDir = project.getDir(); - File classpathFile = new File(projectDir, ".classpath"); //$NON-NLS-1$ - if (classpathFile.exists()) { - String classpathXml = readFile(classpathFile); - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - InputSource is = new InputSource(new StringReader(classpathXml)); - factory.setNamespaceAware(false); - factory.setValidating(false); - try { - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(is); - NodeList tags = document.getElementsByTagName("classpathentry"); //$NON-NLS-1$ - for (int i = 0, n = tags.getLength(); i < n; i++) { - Element element = (Element) tags.item(i); - String kind = element.getAttribute("kind"); //$NON-NLS-1$ - List<File> addTo = null; - if (kind.equals("src")) { //$NON-NLS-1$ - addTo = sources; - } else if (kind.equals("output")) { //$NON-NLS-1$ - addTo = classes; - } else if (kind.equals("lib")) { //$NON-NLS-1$ - addTo = libraries; - } - if (addTo != null) { - String path = element.getAttribute("path"); //$NON-NLS-1$ - File folder = new File(projectDir, path); - if (folder.exists()) { - addTo.add(folder); - } - } - } - } catch (Exception e) { - log(null, null); - } - } - - // Add in libraries that aren't specified in the .classpath file - File libs = new File(project.getDir(), LIBS_FOLDER); - if (libs.isDirectory()) { - File[] jars = libs.listFiles(); - if (jars != null) { - for (File jar : jars) { - if (LintUtils.endsWith(jar.getPath(), DOT_JAR) - && !libraries.contains(jar)) { - libraries.add(jar); - } - } - } - } - - if (classes.isEmpty()) { - File folder = new File(projectDir, CLASS_FOLDER); - if (folder.exists()) { - classes.add(folder); - } else { - // Maven checks - folder = new File(projectDir, - "target" + File.separator + "classes"); //$NON-NLS-1$ //$NON-NLS-2$ - if (folder.exists()) { - classes.add(folder); - - // If it's maven, also correct the source path, "src" works but - // it's in a more specific subfolder - if (sources.isEmpty()) { - File src = new File(projectDir, - "src" + File.separator //$NON-NLS-1$ - + "main" + File.separator //$NON-NLS-1$ - + "java"); //$NON-NLS-1$ - if (src.exists()) { - sources.add(src); - } else { - src = new File(projectDir, SRC_FOLDER); - if (src.exists()) { - sources.add(src); - } - } - - File gen = new File(projectDir, - "target" + File.separator //$NON-NLS-1$ - + "generated-sources" + File.separator //$NON-NLS-1$ - + "r"); //$NON-NLS-1$ - if (gen.exists()) { - sources.add(gen); - } - } - } - } - } - - // Fallback, in case there is no Eclipse project metadata here - if (sources.isEmpty()) { - File src = new File(projectDir, SRC_FOLDER); - if (src.exists()) { - sources.add(src); - } - File gen = new File(projectDir, GEN_FOLDER); - if (gen.exists()) { - sources.add(gen); - } - } - - info = new ClassPathInfo(sources, classes, libraries); - mProjectInfo.put(project, info); - } - - return info; - } - - /** - * A map from directory to existing projects, or null. Used to ensure that - * projects are unique for a directory (in case we process a library project - * before its including project for example) - */ - private Map<File, Project> mDirToProject; - - /** - * Returns a project for the given directory. This should return the same - * project for the same directory if called repeatedly. - * - * @param dir the directory containing the project - * @param referenceDir See {@link Project#getReferenceDir()}. - * @return a project, never null - */ - @NonNull - public Project getProject(@NonNull File dir, @NonNull File referenceDir) { - if (mDirToProject == null) { - mDirToProject = new HashMap<File, Project>(); - } - - File canonicalDir = dir; - try { - // Attempt to use the canonical handle for the file, in case there - // are symlinks etc present (since when handling library projects, - // we also call getCanonicalFile to compute the result of appending - // relative paths, which can then resolve symlinks and end up with - // a different prefix) - canonicalDir = dir.getCanonicalFile(); - } catch (IOException ioe) { - // pass - } - - Project project = mDirToProject.get(canonicalDir); - if (project != null) { - return project; - } - - project = createProject(dir, referenceDir); - mDirToProject.put(canonicalDir, project); - return project; - } - - private Set<File> mProjectDirs = Sets.newHashSet(); - - /** - * Create a project for the given directory - * @param dir the root directory of the project - * @param referenceDir See {@link Project#getReferenceDir()}. - * @return a new project - */ - @NonNull - protected Project createProject(@NonNull File dir, @NonNull File referenceDir) { - if (mProjectDirs.contains(dir)) { - throw new CircularDependencyException( - "Circular library dependencies; check your project.properties files carefully"); - } - mProjectDirs.add(dir); - return Project.create(this, dir, referenceDir); - } - - /** - * Returns the name of the given project - * - * @param project the project to look up - * @return the name of the project - */ - @NonNull - public String getProjectName(@NonNull Project project) { - return project.getDir().getName(); - } - - private IAndroidTarget[] mTargets; - - /** - * Returns all the {@link IAndroidTarget} versions installed in the user's SDK install - * area. - * - * @return all the installed targets - */ - @NonNull - public IAndroidTarget[] getTargets() { - if (mTargets == null) { - File sdkHome = getSdkHome(); - if (sdkHome != null) { - StdLogger log = new StdLogger(Level.WARNING); - SdkManager manager = SdkManager.createManager(sdkHome.getPath(), log); - mTargets = manager.getTargets(); - } else { - mTargets = new IAndroidTarget[0]; - } - } - - return mTargets; - } - - /** - * Returns the highest known API level. - * - * @return the highest known API level - */ - public int getHighestKnownApiLevel() { - int max = SdkConstants.HIGHEST_KNOWN_API; - - for (IAndroidTarget target : getTargets()) { - if (target.isPlatform()) { - int api = target.getVersion().getApiLevel(); - if (api > max && !target.getVersion().isPreview()) { - max = api; - } - } - } - - return max; - } - - /** - * Returns the super class for the given class name, which should be in VM - * format (e.g. java/lang/Integer, not java.lang.Integer, and using $ rather - * than . for inner classes). If the super class is not known, returns null. - * <p> - * This is typically not necessary, since lint analyzes all the available - * classes. However, if this lint client is invoking lint in an incremental - * context (for example, an IDE offering incremental analysis of a single - * source file), then lint may not see all the classes, and the client can - * provide its own super class lookup. - * - * @param project the project containing the class - * @param name the fully qualified class name - * @return the corresponding super class name (in VM format), or null if not - * known - */ - @Nullable - public String getSuperClass(@NonNull Project project, @NonNull String name) { - return null; - } - - /** - * Checks whether the given name is a subclass of the given super class. If - * the method does not know, it should return null, and otherwise return - * {@link Boolean#TRUE} or {@link Boolean#FALSE}. - * <p> - * Note that the class names are in internal VM format (java/lang/Integer, - * not java.lang.Integer, and using $ rather than . for inner classes). - * - * @param project the project context to look up the class in - * @param name the name of the class to be checked - * @param superClassName the name of the super class to compare to - * @return true if the class of the given name extends the given super class - */ - @Nullable - public Boolean isSubclassOf( - @NonNull Project project, - @NonNull String name, @NonNull - String superClassName) { - return null; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintDriver.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintDriver.java deleted file mode 100644 index daf2a07..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintDriver.java +++ /dev/null @@ -1,2227 +0,0 @@ -/* - * 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.client.api; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ATTR_IGNORE; -import static com.android.SdkConstants.DOT_CLASS; -import static com.android.SdkConstants.DOT_JAR; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.FN_PROJECT_PROGUARD_FILE; -import static com.android.SdkConstants.OLD_PROGUARD_FILE; -import static com.android.SdkConstants.RES_FOLDER; -import static com.android.SdkConstants.SUPPRESS_ALL; -import static com.android.SdkConstants.SUPPRESS_LINT; -import static com.android.SdkConstants.TOOLS_URI; -import static org.objectweb.asm.Opcodes.ASM4; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.resources.ResourceFolderType; -import com.android.sdklib.IAndroidTarget; -import com.android.tools.lint.client.api.LintListener.EventType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import com.google.common.annotations.Beta; -import com.google.common.base.CharMatcher; -import com.google.common.base.Splitter; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.io.ByteStreams; -import com.google.common.io.Closeables; - -import org.objectweb.asm.ClassReader; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.FieldNode; -import org.objectweb.asm.tree.MethodNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import lombok.ast.Annotation; -import lombok.ast.AnnotationElement; -import lombok.ast.AnnotationValue; -import lombok.ast.ArrayInitializer; -import lombok.ast.ClassDeclaration; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.Expression; -import lombok.ast.MethodDeclaration; -import lombok.ast.Modifiers; -import lombok.ast.Node; -import lombok.ast.StrictListAccessor; -import lombok.ast.StringLiteral; -import lombok.ast.TypeReference; -import lombok.ast.VariableDefinition; - -/** - * Analyzes Android projects and files - * <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 class LintDriver { - /** - * Max number of passes to run through the lint runner if requested by - * {@link #requestRepeat} - */ - private static final int MAX_PHASES = 3; - private static final String SUPPRESS_LINT_VMSIG = '/' + SUPPRESS_LINT + ';'; - - private final LintClient mClient; - private final IssueRegistry mRegistry; - private volatile boolean mCanceled; - private EnumSet<Scope> mScope; - private List<? extends Detector> mApplicableDetectors; - private Map<Scope, List<Detector>> mScopeDetectors; - private List<LintListener> mListeners; - private int mPhase; - private List<Detector> mRepeatingDetectors; - private EnumSet<Scope> mRepeatScope; - private Project[] mCurrentProjects; - private Project mCurrentProject; - private boolean mAbbreviating = true; - private boolean mParserErrors; - private Map<Object,Object> mProperties; - - /** - * Creates a new {@link LintDriver} - * - * @param registry The registry containing issues to be checked - * @param client the tool wrapping the analyzer, such as an IDE or a CLI - */ - public LintDriver(@NonNull IssueRegistry registry, @NonNull LintClient client) { - mRegistry = registry; - mClient = new LintClientWrapper(client); - } - - /** Cancels the current lint run as soon as possible */ - public void cancel() { - mCanceled = true; - } - - /** - * Returns the scope for the lint job - * - * @return the scope, never null - */ - @NonNull - public EnumSet<Scope> getScope() { - return mScope; - } - - /** - * Returns the lint client requesting the lint check - * - * @return the client, never null - */ - @NonNull - public LintClient getClient() { - return mClient; - } - - /** - * Records a property for later retrieval by {@link #getProperty(Object)} - * - * @param key the key to associate the value with - * @param value the value, or null to remove a previous binding - */ - public void putProperty(@NonNull Object key, @Nullable Object value) { - if (mProperties == null) { - mProperties = Maps.newHashMap(); - } - if (value == null) { - mProperties.remove(key); - } else { - mProperties.put(key, value); - } - } - - /** - * Returns the property previously stored with the given key, or null - * - * @param key the key - * @return the value or null if not found - */ - @Nullable - public Object getProperty(@NonNull Object key) { - if (mProperties != null) { - return mProperties.get(key); - } - - return null; - } - - /** - * Returns the current phase number. The first pass is numbered 1. Only one pass - * will be performed, unless a {@link Detector} calls {@link #requestRepeat}. - * - * @return the current phase, usually 1 - */ - public int getPhase() { - return mPhase; - } - - /** - * Returns the current {@link IssueRegistry}. - * - * @return the current {@link IssueRegistry} - */ - @NonNull - public IssueRegistry getRegistry() { - return mRegistry; - } - - /** - * Returns the project containing a given file, or null if not found. This searches - * only among the currently checked project and its library projects, not among all - * possible projects being scanned sequentially. - * - * @param file the file to be checked - * @return the corresponding project, or null if not found - */ - @Nullable - public Project findProjectFor(@NonNull File file) { - if (mCurrentProjects != null) { - if (mCurrentProjects.length == 1) { - return mCurrentProjects[0]; - } - String path = file.getPath(); - for (Project project : mCurrentProjects) { - if (path.startsWith(project.getDir().getPath())) { - return project; - } - } - } - - return null; - } - - /** - * Sets whether lint should abbreviate output when appropriate. - * - * @param abbreviating true to abbreviate output, false to include everything - */ - public void setAbbreviating(boolean abbreviating) { - mAbbreviating = abbreviating; - } - - /** - * Returns whether lint should abbreviate output when appropriate. - * - * @return true if lint should abbreviate output, false when including everything - */ - public boolean isAbbreviating() { - return mAbbreviating; - } - - /** - * Returns whether lint has encountered any files with fatal parser errors - * (e.g. broken source code, or even broken parsers) - * <p> - * This is useful for checks that need to make sure they've seen all data in - * order to be conclusive (such as an unused resource check). - * - * @return true if any files were not properly processed because they - * contained parser errors - */ - public boolean hasParserErrors() { - return mParserErrors; - } - - /** - * Sets whether lint has encountered files with fatal parser errors. - * - * @see #hasParserErrors() - * @param hasErrors whether parser errors have been encountered - */ - public void setHasParserErrors(boolean hasErrors) { - mParserErrors = hasErrors; - } - - /** - * Returns the projects being analyzed - * - * @return the projects being analyzed - */ - @NonNull - public List<Project> getProjects() { - if (mCurrentProjects != null) { - return Arrays.asList(mCurrentProjects); - } - return Collections.emptyList(); - } - - /** - * Analyze the given file (which can point to an Android project). Issues found - * are reported to the associated {@link LintClient}. - * - * @param files the files and directories to be analyzed - * @param scope the scope of the analysis; detectors with a wider scope will - * not be run. If null, the scope will be inferred from the files. - */ - public void analyze(@NonNull List<File> files, @Nullable EnumSet<Scope> scope) { - mCanceled = false; - mScope = scope; - - Collection<Project> projects; - try { - projects = computeProjects(files); - } catch (CircularDependencyException e) { - mCurrentProject = e.getProject(); - if (mCurrentProject != null) { - File file = e.getLocation().getFile(); - Context context = new Context(this, mCurrentProject, null, file); - context.report(IssueRegistry.LINT_ERROR, e.getLocation(), e.getMessage(), null); - mCurrentProject = null; - } - return; - } - if (projects.isEmpty()) { - mClient.log(null, "No projects found for %1$s", files.toString()); - return; - } - if (mCanceled) { - return; - } - - if (mScope == null) { - // Infer the scope - mScope = EnumSet.noneOf(Scope.class); - for (Project project : projects) { - List<File> subset = project.getSubset(); - if (subset != null) { - for (File file : subset) { - String name = file.getName(); - if (name.equals(ANDROID_MANIFEST_XML)) { - mScope.add(Scope.MANIFEST); - } else if (name.endsWith(DOT_XML)) { - mScope.add(Scope.RESOURCE_FILE); - } else if (name.equals(RES_FOLDER) - || file.getParent().equals(RES_FOLDER)) { - mScope.add(Scope.ALL_RESOURCE_FILES); - mScope.add(Scope.RESOURCE_FILE); - } else if (name.endsWith(DOT_JAVA)) { - mScope.add(Scope.JAVA_FILE); - } else if (name.endsWith(DOT_CLASS)) { - mScope.add(Scope.CLASS_FILE); - } else if (name.equals(OLD_PROGUARD_FILE) - || name.equals(FN_PROJECT_PROGUARD_FILE)) { - mScope.add(Scope.PROGUARD_FILE); - } - } - } else { - // Specified a full project: just use the full project scope - mScope = Scope.ALL; - break; - } - } - } - - fireEvent(EventType.STARTING, null); - - for (Project project : projects) { - mPhase = 1; - - // The set of available detectors varies between projects - computeDetectors(project); - - if (mApplicableDetectors.isEmpty()) { - // No detectors enabled in this project: skip it - continue; - } - - checkProject(project); - if (mCanceled) { - break; - } - - runExtraPhases(project); - } - - fireEvent(mCanceled ? EventType.CANCELED : EventType.COMPLETED, null); - } - - private void runExtraPhases(Project project) { - // Did any detectors request another phase? - if (mRepeatingDetectors != null) { - // Yes. Iterate up to MAX_PHASES times. - - // During the extra phases, we might be narrowing the scope, and setting it in the - // scope field such that detectors asking about the available scope will get the - // correct result. However, we need to restore it to the original scope when this - // is done in case there are other projects that will be checked after this, since - // the repeated phases is done *per project*, not after all projects have been - // processed. - EnumSet<Scope> oldScope = mScope; - - do { - mPhase++; - fireEvent(EventType.NEW_PHASE, - new Context(this, project, null, project.getDir())); - - // Narrow the scope down to the set of scopes requested by - // the rules. - if (mRepeatScope == null) { - mRepeatScope = Scope.ALL; - } - mScope = Scope.intersect(mScope, mRepeatScope); - if (mScope.isEmpty()) { - break; - } - - // Compute the detectors to use for this pass. - // Unlike the normal computeDetectors(project) call, - // this is going to use the existing instances, and include - // those that apply for the configuration. - computeRepeatingDetectors(mRepeatingDetectors, project); - - if (mApplicableDetectors.isEmpty()) { - // No detectors enabled in this project: skip it - continue; - } - - checkProject(project); - if (mCanceled) { - break; - } - } while (mPhase < MAX_PHASES && mRepeatingDetectors != null); - - mScope = oldScope; - } - } - - private void computeRepeatingDetectors(List<Detector> detectors, Project project) { - // Ensure that the current visitor is recomputed - mCurrentFolderType = null; - mCurrentVisitor = null; - - // Create map from detector class to issue such that we can - // compute applicable issues for each detector in the list of detectors - // to be repeated - List<Issue> issues = mRegistry.getIssues(); - Multimap<Class<? extends Detector>, Issue> issueMap = - ArrayListMultimap.create(issues.size(), 3); - for (Issue issue : issues) { - issueMap.put(issue.getDetectorClass(), issue); - } - - Map<Class<? extends Detector>, EnumSet<Scope>> detectorToScope = - new HashMap<Class<? extends Detector>, EnumSet<Scope>>(); - Map<Scope, List<Detector>> scopeToDetectors = - new EnumMap<Scope, List<Detector>>(Scope.class); - - List<Detector> detectorList = new ArrayList<Detector>(); - // Compute the list of detectors (narrowed down from mRepeatingDetectors), - // and simultaneously build up the detectorToScope map which tracks - // the scopes each detector is affected by (this is used to populate - // the mScopeDetectors map which is used during iteration). - Configuration configuration = project.getConfiguration(); - for (Detector detector : detectors) { - Class<? extends Detector> detectorClass = detector.getClass(); - Collection<Issue> detectorIssues = issueMap.get(detectorClass); - if (detectorIssues != null) { - boolean add = false; - for (Issue issue : detectorIssues) { - // The reason we have to check whether the detector is enabled - // is that this is a per-project property, so when running lint in multiple - // projects, a detector enabled only in a different project could have - // requested another phase, and we end up in this project checking whether - // the detector is enabled here. - if (!configuration.isEnabled(issue)) { - continue; - } - - add = true; // Include detector if any of its issues are enabled - - EnumSet<Scope> s = detectorToScope.get(detectorClass); - EnumSet<Scope> issueScope = issue.getScope(); - if (s == null) { - detectorToScope.put(detectorClass, issueScope); - } else if (!s.containsAll(issueScope)) { - EnumSet<Scope> union = EnumSet.copyOf(s); - union.addAll(issueScope); - detectorToScope.put(detectorClass, union); - } - } - - if (add) { - detectorList.add(detector); - EnumSet<Scope> union = detectorToScope.get(detector.getClass()); - for (Scope s : union) { - List<Detector> list = scopeToDetectors.get(s); - if (list == null) { - list = new ArrayList<Detector>(); - scopeToDetectors.put(s, list); - } - list.add(detector); - } - } - } - } - - mApplicableDetectors = detectorList; - mScopeDetectors = scopeToDetectors; - mRepeatingDetectors = null; - mRepeatScope = null; - - validateScopeList(); - } - - private void computeDetectors(@NonNull Project project) { - // Ensure that the current visitor is recomputed - mCurrentFolderType = null; - mCurrentVisitor = null; - - Configuration configuration = project.getConfiguration(); - mScopeDetectors = new EnumMap<Scope, List<Detector>>(Scope.class); - mApplicableDetectors = mRegistry.createDetectors(mClient, configuration, - mScope, mScopeDetectors); - - validateScopeList(); - } - - /** Development diagnostics only, run with assertions on */ - @SuppressWarnings("all") // Turn off warnings for the intentional assertion side effect below - private void validateScopeList() { - boolean assertionsEnabled = false; - assert assertionsEnabled = true; // Intentional side-effect - if (assertionsEnabled) { - List<Detector> resourceFileDetectors = mScopeDetectors.get(Scope.RESOURCE_FILE); - if (resourceFileDetectors != null) { - for (Detector detector : resourceFileDetectors) { - assert detector instanceof ResourceXmlDetector : detector; - } - } - - List<Detector> manifestDetectors = mScopeDetectors.get(Scope.MANIFEST); - if (manifestDetectors != null) { - for (Detector detector : manifestDetectors) { - assert detector instanceof Detector.XmlScanner : detector; - } - } - List<Detector> javaCodeDetectors = mScopeDetectors.get(Scope.ALL_JAVA_FILES); - if (javaCodeDetectors != null) { - for (Detector detector : javaCodeDetectors) { - assert detector instanceof Detector.JavaScanner : detector; - } - } - List<Detector> javaFileDetectors = mScopeDetectors.get(Scope.JAVA_FILE); - if (javaFileDetectors != null) { - for (Detector detector : javaFileDetectors) { - assert detector instanceof Detector.JavaScanner : detector; - } - } - - List<Detector> classDetectors = mScopeDetectors.get(Scope.CLASS_FILE); - if (classDetectors != null) { - for (Detector detector : classDetectors) { - assert detector instanceof Detector.ClassScanner : detector; - } - } - - List<Detector> classCodeDetectors = mScopeDetectors.get(Scope.ALL_CLASS_FILES); - if (classCodeDetectors != null) { - for (Detector detector : classCodeDetectors) { - assert detector instanceof Detector.ClassScanner : detector; - } - } - - List<Detector> otherDetectors = mScopeDetectors.get(Scope.OTHER_SCOPE); - if (otherDetectors != null) { - for (Detector detector : otherDetectors) { - assert detector instanceof Detector.OtherFileScanner : detector; - } - } - } - } - - private void registerProjectFile( - @NonNull Map<File, Project> fileToProject, - @NonNull File file, - @NonNull File projectDir, - @NonNull File rootDir) { - fileToProject.put(file, mClient.getProject(projectDir, rootDir)); - } - - private Collection<Project> computeProjects(@NonNull List<File> files) { - // Compute list of projects - Map<File, Project> fileToProject = new HashMap<File, Project>(); - - File sharedRoot = null; - - // Ensure that we have absolute paths such that if you lint - // "foo bar" in "baz" we can show baz/ as the root - if (files.size() > 1) { - List<File> absolute = new ArrayList<File>(files.size()); - for (File file : files) { - absolute.add(file.getAbsoluteFile()); - } - files = absolute; - - sharedRoot = LintUtils.getCommonParent(files); - if (sharedRoot != null && sharedRoot.getParentFile() == null) { // "/" ? - sharedRoot = null; - } - } - - - for (File file : files) { - if (file.isDirectory()) { - File rootDir = sharedRoot; - if (rootDir == null) { - rootDir = file; - if (files.size() > 1) { - rootDir = file.getParentFile(); - if (rootDir == null) { - rootDir = file; - } - } - } - - // Figure out what to do with a directory. Note that the meaning of the - // directory can be ambiguous: - // If you pass a directory which is unknown, we don't know if we should - // search upwards (in case you're pointing at a deep java package folder - // within the project), or if you're pointing at some top level directory - // containing lots of projects you want to scan. We attempt to do the - // right thing, which is to see if you're pointing right at a project or - // right within it (say at the src/ or res/) folder, and if not, you're - // hopefully pointing at a project tree that you want to scan recursively. - if (LintUtils.isProjectDir(file)) { - registerProjectFile(fileToProject, file, file, rootDir); - continue; - } else { - File parent = file.getParentFile(); - if (parent != null) { - if (LintUtils.isProjectDir(parent)) { - registerProjectFile(fileToProject, file, parent, parent); - continue; - } else { - parent = parent.getParentFile(); - if (parent != null && LintUtils.isProjectDir(parent)) { - registerProjectFile(fileToProject, file, parent, parent); - continue; - } - } - } - - // Search downwards for nested projects - addProjects(file, fileToProject, rootDir); - } - } else { - // Pointed at a file: Search upwards for the containing project - File parent = file.getParentFile(); - while (parent != null) { - if (LintUtils.isProjectDir(parent)) { - registerProjectFile(fileToProject, file, parent, parent); - break; - } - parent = parent.getParentFile(); - } - } - - if (mCanceled) { - return Collections.emptySet(); - } - } - - for (Map.Entry<File, Project> entry : fileToProject.entrySet()) { - File file = entry.getKey(); - Project project = entry.getValue(); - if (!file.equals(project.getDir())) { - if (file.isDirectory()) { - try { - File dir = file.getCanonicalFile(); - if (dir.equals(project.getDir())) { - continue; - } - } catch (IOException ioe) { - // pass - } - } - - project.addFile(file); - } - } - - // Partition the projects up such that we only return projects that aren't - // included by other projects (e.g. because they are library projects) - - Collection<Project> allProjects = fileToProject.values(); - Set<Project> roots = new HashSet<Project>(allProjects); - for (Project project : allProjects) { - roots.removeAll(project.getAllLibraries()); - } - - // Report issues for all projects that are explicitly referenced. We need to - // do this here, since the project initialization will mark all library - // projects as no-report projects by default. - for (Project project : allProjects) { - // Report issues for all projects explicitly listed or found via a directory - // traversal -- including library projects. - project.setReportIssues(true); - } - - if (LintUtils.assertionsEnabled()) { - // Make sure that all the project directories are unique. This ensures - // that we didn't accidentally end up with different project instances - // for a library project discovered as a directory as well as one - // initialized from the library project dependency list - IdentityHashMap<Project, Project> projects = - new IdentityHashMap<Project, Project>(); - for (Project project : roots) { - projects.put(project, project); - for (Project library : project.getAllLibraries()) { - projects.put(library, library); - } - } - Set<File> dirs = new HashSet<File>(); - for (Project project : projects.keySet()) { - assert !dirs.contains(project.getDir()); - dirs.add(project.getDir()); - } - } - - return roots; - } - - private void addProjects( - @NonNull File dir, - @NonNull Map<File, Project> fileToProject, - @NonNull File rootDir) { - if (mCanceled) { - return; - } - - if (LintUtils.isProjectDir(dir)) { - registerProjectFile(fileToProject, dir, dir, rootDir); - } else { - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - addProjects(file, fileToProject, rootDir); - } - } - } - } - } - - private void checkProject(@NonNull Project project) { - File projectDir = project.getDir(); - - Context projectContext = new Context(this, project, null, projectDir); - fireEvent(EventType.SCANNING_PROJECT, projectContext); - - List<Project> allLibraries = project.getAllLibraries(); - Set<Project> allProjects = new HashSet<Project>(allLibraries.size() + 1); - allProjects.add(project); - allProjects.addAll(allLibraries); - mCurrentProjects = allProjects.toArray(new Project[allProjects.size()]); - - mCurrentProject = project; - - for (Detector check : mApplicableDetectors) { - check.beforeCheckProject(projectContext); - if (mCanceled) { - return; - } - } - - assert mCurrentProject == project; - runFileDetectors(project, project); - - if (!Scope.checkSingleFile(mScope)) { - List<Project> libraries = project.getDirectLibraries(); - for (Project library : libraries) { - Context libraryContext = new Context(this, library, project, projectDir); - fireEvent(EventType.SCANNING_LIBRARY_PROJECT, libraryContext); - mCurrentProject = library; - - for (Detector check : mApplicableDetectors) { - check.beforeCheckLibraryProject(libraryContext); - if (mCanceled) { - return; - } - } - assert mCurrentProject == library; - - runFileDetectors(library, project); - if (mCanceled) { - return; - } - - assert mCurrentProject == library; - - for (Detector check : mApplicableDetectors) { - check.afterCheckLibraryProject(libraryContext); - if (mCanceled) { - return; - } - } - } - } - - mCurrentProject = project; - - for (Detector check : mApplicableDetectors) { - check.afterCheckProject(projectContext); - if (mCanceled) { - return; - } - } - - if (mCanceled) { - mClient.report( - projectContext, - // Must provide an issue since API guarantees that the issue parameter - // is valid - Issue.create("Lint", "", "", Category.PERFORMANCE, 0, Severity.INFORMATIONAL, //$NON-NLS-1$ - Detector.class, EnumSet.noneOf(Scope.class)), - Severity.INFORMATIONAL, - null /*range*/, - "Lint canceled by user", null); - } - - mCurrentProjects = null; - } - - private void runFileDetectors(@NonNull Project project, @Nullable Project main) { - // Look up manifest information (but not for library projects) - File manifestFile = project.getManifestFile(); - if (manifestFile != null) { - XmlContext context = new XmlContext(this, project, main, manifestFile, null); - IDomParser parser = mClient.getDomParser(); - if (parser != null) { - context.document = parser.parseXml(context); - if (context.document != null) { - try { - project.readManifest(context.document); - - if ((!project.isLibrary() || (main != null && main.isMergingManifests())) - && mScope.contains(Scope.MANIFEST)) { - List<Detector> detectors = mScopeDetectors.get(Scope.MANIFEST); - if (detectors != null) { - XmlVisitor v = new XmlVisitor(parser, detectors); - fireEvent(EventType.SCANNING_FILE, context); - v.visitFile(context, manifestFile); - } - } - } finally { - if (context.document != null) { // else: freed by XmlVisitor above - parser.dispose(context, context.document); - } - } - } - } - } - - // Process both Scope.RESOURCE_FILE and Scope.ALL_RESOURCE_FILES detectors together - // in a single pass through the resource directories. - if (mScope.contains(Scope.ALL_RESOURCE_FILES) || mScope.contains(Scope.RESOURCE_FILE)) { - List<Detector> checks = union(mScopeDetectors.get(Scope.RESOURCE_FILE), - mScopeDetectors.get(Scope.ALL_RESOURCE_FILES)); - if (checks != null && !checks.isEmpty()) { - List<ResourceXmlDetector> xmlDetectors = - new ArrayList<ResourceXmlDetector>(checks.size()); - for (Detector detector : checks) { - if (detector instanceof ResourceXmlDetector) { - xmlDetectors.add((ResourceXmlDetector) detector); - } - } - if (!xmlDetectors.isEmpty()) { - List<File> files = project.getSubset(); - if (files != null) { - checkIndividualResources(project, main, xmlDetectors, files); - } else { - List<File> resourceFolders = project.getResourceFolders(); - if (!resourceFolders.isEmpty() && !xmlDetectors.isEmpty()) { - for (File res : resourceFolders) { - checkResFolder(project, main, res, xmlDetectors); - } - } - } - } - } - } - - if (mCanceled) { - return; - } - - if (mScope.contains(Scope.JAVA_FILE) || mScope.contains(Scope.ALL_JAVA_FILES)) { - List<Detector> checks = union(mScopeDetectors.get(Scope.JAVA_FILE), - mScopeDetectors.get(Scope.ALL_JAVA_FILES)); - if (checks != null && !checks.isEmpty()) { - List<File> files = project.getSubset(); - if (files != null) { - checkIndividualJavaFiles(project, main, checks, files); - } else { - List<File> sourceFolders = project.getJavaSourceFolders(); - checkJava(project, main, sourceFolders, checks); - } - } - } - - if (mCanceled) { - return; - } - - if (mScope.contains(Scope.CLASS_FILE) - || mScope.contains(Scope.ALL_CLASS_FILES) - || mScope.contains(Scope.JAVA_LIBRARIES)) { - checkClasses(project, main); - } - - if (mScope.contains(Scope.OTHER)) { - List<Detector> checks = mScopeDetectors.get(Scope.OTHER); - if (checks != null) { - OtherFileVisitor visitor = new OtherFileVisitor(checks); - visitor.scan(this, project, main); - } - } - - if (mCanceled) { - return; - } - - if (project == main && mScope.contains(Scope.PROGUARD_FILE)) { - checkProGuard(project, main); - } - } - - private void checkProGuard(Project project, Project main) { - List<Detector> detectors = mScopeDetectors.get(Scope.PROGUARD_FILE); - if (detectors != null) { - Project p = main != null ? main : project; - List<File> files = new ArrayList<File>(); - String paths = p.getProguardPath(); - if (paths != null) { - Splitter splitter = Splitter.on(CharMatcher.anyOf(":;")); //$NON-NLS-1$ - for (String path : splitter.split(paths)) { - if (path.contains("${")) { //$NON-NLS-1$ - // Don't analyze the global/user proguard files - continue; - } - File file = new File(path); - if (!file.isAbsolute()) { - file = new File(project.getDir(), path); - } - if (file.exists()) { - files.add(file); - } - } - } - if (files.isEmpty()) { - File file = new File(project.getDir(), OLD_PROGUARD_FILE); - if (file.exists()) { - files.add(file); - } - file = new File(project.getDir(), FN_PROJECT_PROGUARD_FILE); - if (file.exists()) { - files.add(file); - } - } - for (File file : files) { - Context context = new Context(this, project, main, file); - fireEvent(EventType.SCANNING_FILE, context); - for (Detector detector : detectors) { - if (detector.appliesTo(context, file)) { - detector.beforeCheckFile(context); - detector.run(context); - detector.afterCheckFile(context); - } - } - } - } - } - - /** True if execution has been canceled */ - boolean isCanceled() { - return mCanceled; - } - - /** - * Map from VM class name to corresponding super class VM name, if available. - * This map is typically null except <b>during</b> class processing. - */ - private Map<String, String> mSuperClassMap; - - /** - * Returns the super class for the given class name, - * which should be in VM format (e.g. java/lang/Integer, not java.lang.Integer). - * If the super class is not known, returns null. This can happen if - * the given class is not a known class according to the project or its - * libraries, for example because it refers to one of the core libraries which - * are not analyzed by lint. - * - * @param name the fully qualified class name - * @return the corresponding super class name (in VM format), or null if not known - */ - @Nullable - public String getSuperClass(@NonNull String name) { - if (mSuperClassMap == null) { - throw new IllegalStateException("Only callable during ClassScanner#checkClass"); - } - assert name.indexOf('.') == -1 : "Use VM signatures, e.g. java/lang/Integer"; - - String superClass = mSuperClassMap.get(name); - if (superClass == null && mCurrentProject != null) { - if ("java/lang/Object".equals(name)) { //$NON-NLS-1$ - return null; - } - superClass = mClient.getSuperClass(mCurrentProject, name); - if (superClass != null) { - mSuperClassMap.put(name, superClass); - } - } - - return superClass; - } - - /** - * Returns true if the given class is a subclass of the given super class. - * - * @param classNode the class to check whether it is a subclass of the given - * super class name - * @param superClassName the fully qualified super class name (in VM format, - * e.g. java/lang/Integer, not java.lang.Integer. - * @return true if the given class is a subclass of the given super class - */ - public boolean isSubclassOf(@NonNull ClassNode classNode, @NonNull String superClassName) { - if (superClassName.equals(classNode.superName)) { - return true; - } - - if (mCurrentProject != null) { - Boolean isSub = mClient.isSubclassOf(mCurrentProject, classNode.name, superClassName); - if (isSub != null) { - return isSub.booleanValue(); - } - } - - String className = classNode.name; - while (className != null) { - if (className.equals(superClassName)) { - return true; - } - className = getSuperClass(className); - } - - return false; - } - @Nullable - private static List<Detector> union( - @Nullable List<Detector> list1, - @Nullable List<Detector> list2) { - if (list1 == null) { - return list2; - } else if (list2 == null) { - return list1; - } else { - // Use set to pick out unique detectors, since it's possible for there to be overlap, - // e.g. the DuplicateIdDetector registers both a cross-resource issue and a - // single-file issue, so it shows up on both scope lists: - Set<Detector> set = new HashSet<Detector>(list1.size() + list2.size()); - if (list1 != null) { - set.addAll(list1); - } - if (list2 != null) { - set.addAll(list2); - } - - return new ArrayList<Detector>(set); - } - } - - /** Check the classes in this project (and if applicable, in any library projects */ - private void checkClasses(Project project, Project main) { - List<File> files = project.getSubset(); - if (files != null) { - checkIndividualClassFiles(project, main, files); - return; - } - - // We need to read in all the classes up front such that we can initialize - // the parent chains (such that for example for a virtual dispatch, we can - // also check the super classes). - - List<File> libraries = project.getJavaLibraries(); - List<ClassEntry> libraryEntries; - if (!libraries.isEmpty()) { - libraryEntries = new ArrayList<ClassEntry>(64); - findClasses(libraryEntries, libraries); - Collections.sort(libraryEntries); - } else { - libraryEntries = Collections.emptyList(); - } - - List<File> classFolders = project.getJavaClassFolders(); - List<ClassEntry> classEntries; - if (classFolders.isEmpty()) { - String message = String.format("No .class files were found in project \"%1$s\", " - + "so none of the classfile based checks could be run. " - + "Does the project need to be built first?", project.getName()); - Location location = Location.create(project.getDir()); - mClient.report(new Context(this, project, main, project.getDir()), - IssueRegistry.LINT_ERROR, - project.getConfiguration().getSeverity(IssueRegistry.LINT_ERROR), - location, message, null); - classEntries = Collections.emptyList(); - } else { - classEntries = new ArrayList<ClassEntry>(64); - findClasses(classEntries, classFolders); - Collections.sort(classEntries); - } - - if (getPhase() == 1) { - mSuperClassMap = getSuperMap(libraryEntries, classEntries); - } - - // Actually run the detectors. Libraries should be called before the - // main classes. - runClassDetectors(Scope.JAVA_LIBRARIES, libraryEntries, project, main); - - if (mCanceled) { - return; - } - - runClassDetectors(Scope.CLASS_FILE, classEntries, project, main); - runClassDetectors(Scope.ALL_CLASS_FILES, classEntries, project, main); - } - - private void checkIndividualClassFiles( - @NonNull Project project, - @Nullable Project main, - @NonNull List<File> files) { - List<ClassEntry> entries = new ArrayList<ClassEntry>(files.size()); - - List<File> classFolders = project.getJavaClassFolders(); - if (!classFolders.isEmpty()) { - for (File file : files) { - String path = file.getPath(); - if (file.isFile() && path.endsWith(DOT_CLASS)) { - try { - byte[] bytes = mClient.readBytes(file); - if (bytes != null) { - for (File dir : classFolders) { - if (path.startsWith(dir.getPath())) { - entries.add(new ClassEntry(file, null /* jarFile*/, dir, - bytes)); - break; - } - } - } - } catch (IOException e) { - mClient.log(e, null); - continue; - } - - if (mCanceled) { - return; - } - } - } - - if (!entries.isEmpty()) { - Collections.sort(entries); - // No superclass info available on individual lint runs, unless - // the client can provide it - mSuperClassMap = Maps.newHashMap(); - runClassDetectors(Scope.CLASS_FILE, entries, project, main); - } - } - } - - /** - * Stack of {@link ClassNode} nodes for outer classes of the currently - * processed class, including that class itself. Populated by - * {@link #runClassDetectors(Scope, List, Project, Project)} and used by - * {@link #getOuterClassNode(ClassNode)} - */ - private Deque<ClassNode> mOuterClasses; - - private void runClassDetectors(Scope scope, List<ClassEntry> entries, - Project project, Project main) { - if (mScope.contains(scope)) { - List<Detector> classDetectors = mScopeDetectors.get(scope); - if (classDetectors != null && !classDetectors.isEmpty() && !entries.isEmpty()) { - AsmVisitor visitor = new AsmVisitor(mClient, classDetectors); - - String sourceContents = null; - String sourceName = ""; - mOuterClasses = new ArrayDeque<ClassNode>(); - for (ClassEntry entry : entries) { - ClassReader reader; - ClassNode classNode; - try { - reader = new ClassReader(entry.bytes); - classNode = new ClassNode(); - reader.accept(classNode, 0 /* flags */); - } catch (Throwable t) { - mClient.log(null, "Error processing %1$s: broken class file?", - entry.path()); - continue; - } - - ClassNode peek; - while ((peek = mOuterClasses.peek()) != null) { - if (classNode.name.startsWith(peek.name)) { - break; - } else { - mOuterClasses.pop(); - } - } - mOuterClasses.push(classNode); - - if (isSuppressed(null, classNode)) { - // Class was annotated with suppress all -- no need to look any further - continue; - } - - if (sourceContents != null) { - // Attempt to reuse the source buffer if initialized - // This means making sure that the source files - // foo/bar/MyClass and foo/bar/MyClass$Bar - // and foo/bar/MyClass$3 and foo/bar/MyClass$3$1 have the same prefix. - String newName = classNode.name; - int newRootLength = newName.indexOf('$'); - if (newRootLength == -1) { - newRootLength = newName.length(); - } - int oldRootLength = sourceName.indexOf('$'); - if (oldRootLength == -1) { - oldRootLength = sourceName.length(); - } - if (newRootLength != oldRootLength || - !sourceName.regionMatches(0, newName, 0, newRootLength)) { - sourceContents = null; - } - } - - ClassContext context = new ClassContext(this, project, main, - entry.file, entry.jarFile, entry.binDir, entry.bytes, - classNode, scope == Scope.JAVA_LIBRARIES /*fromLibrary*/, - sourceContents); - - try { - visitor.runClassDetectors(context); - } catch (Exception e) { - mClient.log(e, null); - } - - if (mCanceled) { - return; - } - - sourceContents = context.getSourceContents(false/*read*/); - sourceName = classNode.name; - } - - mOuterClasses = null; - } - } - } - - /** Returns the outer class node of the given class node - * @param classNode the inner class node - * @return the outer class node */ - public ClassNode getOuterClassNode(@NonNull ClassNode classNode) { - String outerName = classNode.outerClass; - - Iterator<ClassNode> iterator = mOuterClasses.iterator(); - while (iterator.hasNext()) { - ClassNode node = iterator.next(); - if (outerName != null) { - if (node.name.equals(outerName)) { - return node; - } - } else if (node == classNode) { - return iterator.hasNext() ? iterator.next() : null; - } - } - - return null; - } - - private Map<String, String> getSuperMap(List<ClassEntry> libraryEntries, - List<ClassEntry> classEntries) { - int size = libraryEntries.size() + classEntries.size(); - Map<String, String> map = new HashMap<String, String>(size); - - SuperclassVisitor visitor = new SuperclassVisitor(map); - addSuperClasses(visitor, libraryEntries); - addSuperClasses(visitor, classEntries); - - return map; - } - - private void addSuperClasses(SuperclassVisitor visitor, List<ClassEntry> entries) { - for (ClassEntry entry : entries) { - try { - ClassReader reader = new ClassReader(entry.bytes); - int flags = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG - | ClassReader.SKIP_FRAMES; - reader.accept(visitor, flags); - } catch (Throwable t) { - mClient.log(null, "Error processing %1$s: broken class file?", entry.path()); - } - } - } - - /** Visitor skimming classes and initializing a map of super classes */ - private static class SuperclassVisitor extends ClassVisitor { - private final Map<String, String> mMap; - - public SuperclassVisitor(Map<String, String> map) { - super(ASM4); - mMap = map; - } - - @Override - public void visit(int version, int access, String name, String signature, String superName, - String[] interfaces) { - if (superName != null) { - mMap.put(name, superName); - } - } - } - - private void findClasses( - @NonNull List<ClassEntry> entries, - @NonNull List<File> classPath) { - for (File classPathEntry : classPath) { - if (classPathEntry.getName().endsWith(DOT_JAR)) { - File jarFile = classPathEntry; - if (!jarFile.exists()) { - continue; - } - ZipInputStream zis = null; - try { - FileInputStream fis = new FileInputStream(jarFile); - zis = new ZipInputStream(fis); - ZipEntry entry = zis.getNextEntry(); - while (entry != null) { - String name = entry.getName(); - if (name.endsWith(DOT_CLASS)) { - try { - byte[] bytes = ByteStreams.toByteArray(zis); - if (bytes != null) { - File file = new File(entry.getName()); - entries.add(new ClassEntry(file, jarFile, jarFile, bytes)); - } - } catch (Exception e) { - mClient.log(e, null); - continue; - } - } - - if (mCanceled) { - return; - } - - entry = zis.getNextEntry(); - } - } catch (IOException e) { - mClient.log(e, "Could not read jar file contents from %1$s", jarFile); - } finally { - Closeables.closeQuietly(zis); - } - - continue; - } else if (classPathEntry.isDirectory()) { - File binDir = classPathEntry; - List<File> classFiles = new ArrayList<File>(); - addClassFiles(binDir, classFiles); - - for (File file : classFiles) { - try { - byte[] bytes = mClient.readBytes(file); - if (bytes != null) { - entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes)); - } - } catch (IOException e) { - mClient.log(e, null); - continue; - } - - if (mCanceled) { - return; - } - } - } else { - mClient.log(null, "Ignoring class path entry %1$s", classPathEntry); - } - } - } - - private static void addClassFiles(@NonNull File dir, @NonNull List<File> classFiles) { - // Process the resource folder - File[] files = dir.listFiles(); - if (files != null && files.length > 0) { - for (File file : files) { - if (file.isFile() && file.getName().endsWith(DOT_CLASS)) { - classFiles.add(file); - } else if (file.isDirectory()) { - // Recurse - addClassFiles(file, classFiles); - } - } - } - } - - private void checkJava( - @NonNull Project project, - @Nullable Project main, - @NonNull List<File> sourceFolders, - @NonNull List<Detector> checks) { - IJavaParser javaParser = mClient.getJavaParser(); - if (javaParser == null) { - mClient.log(null, "No java parser provided to lint: not running Java checks"); - return; - } - - assert !checks.isEmpty(); - - // Gather all Java source files in a single pass; more efficient. - List<File> sources = new ArrayList<File>(100); - for (File folder : sourceFolders) { - gatherJavaFiles(folder, sources); - } - if (!sources.isEmpty()) { - JavaVisitor visitor = new JavaVisitor(javaParser, checks); - for (File file : sources) { - JavaContext context = new JavaContext(this, project, main, file); - fireEvent(EventType.SCANNING_FILE, context); - visitor.visitFile(context, file); - if (mCanceled) { - return; - } - } - } - } - - private void checkIndividualJavaFiles( - @NonNull Project project, - @Nullable Project main, - @NonNull List<Detector> checks, - @NonNull List<File> files) { - - IJavaParser javaParser = mClient.getJavaParser(); - if (javaParser == null) { - mClient.log(null, "No java parser provided to lint: not running Java checks"); - return; - } - - JavaVisitor visitor = new JavaVisitor(javaParser, checks); - - for (File file : files) { - if (file.isFile() && file.getPath().endsWith(DOT_JAVA)) { - JavaContext context = new JavaContext(this, project, main, file); - fireEvent(EventType.SCANNING_FILE, context); - visitor.visitFile(context, file); - if (mCanceled) { - return; - } - } - } - } - - private static void gatherJavaFiles(@NonNull File dir, @NonNull List<File> result) { - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isFile() && file.getName().endsWith(".java")) { //$NON-NLS-1$ - result.add(file); - } else if (file.isDirectory()) { - gatherJavaFiles(file, result); - } - } - } - } - - private ResourceFolderType mCurrentFolderType; - private List<ResourceXmlDetector> mCurrentXmlDetectors; - private XmlVisitor mCurrentVisitor; - - @Nullable - private XmlVisitor getVisitor( - @NonNull ResourceFolderType type, - @NonNull List<ResourceXmlDetector> checks) { - if (type != mCurrentFolderType) { - mCurrentFolderType = type; - - // Determine which XML resource detectors apply to the given folder type - List<ResourceXmlDetector> applicableChecks = - new ArrayList<ResourceXmlDetector>(checks.size()); - for (ResourceXmlDetector check : checks) { - if (check.appliesTo(type)) { - applicableChecks.add(check); - } - } - - // If the list of detectors hasn't changed, then just use the current visitor! - if (mCurrentXmlDetectors != null && mCurrentXmlDetectors.equals(applicableChecks)) { - return mCurrentVisitor; - } - - if (applicableChecks.isEmpty()) { - mCurrentVisitor = null; - return null; - } - - IDomParser parser = mClient.getDomParser(); - if (parser != null) { - mCurrentVisitor = new XmlVisitor(parser, applicableChecks); - } - } - - return mCurrentVisitor; - } - - private void checkResFolder( - @NonNull Project project, - @Nullable Project main, - @NonNull File res, - @NonNull List<ResourceXmlDetector> checks) { - assert res.isDirectory(); - File[] resourceDirs = res.listFiles(); - if (resourceDirs == null) { - return; - } - - // Sort alphabetically such that we can process related folder types at the - // same time - - Arrays.sort(resourceDirs); - for (File dir : resourceDirs) { - ResourceFolderType type = ResourceFolderType.getFolderType(dir.getName()); - if (type != null) { - checkResourceFolder(project, main, dir, type, checks); - } - - if (mCanceled) { - return; - } - } - } - - private void checkResourceFolder( - @NonNull Project project, - @Nullable Project main, - @NonNull File dir, - @NonNull ResourceFolderType type, - @NonNull List<ResourceXmlDetector> checks) { - // Process the resource folder - File[] xmlFiles = dir.listFiles(); - if (xmlFiles != null && xmlFiles.length > 0) { - XmlVisitor visitor = getVisitor(type, checks); - if (visitor != null) { // if not, there are no applicable rules in this folder - for (File file : xmlFiles) { - if (LintUtils.isXmlFile(file)) { - XmlContext context = new XmlContext(this, project, main, file, type); - fireEvent(EventType.SCANNING_FILE, context); - visitor.visitFile(context, file); - if (mCanceled) { - return; - } - } - } - } - } - } - - /** Checks individual resources */ - private void checkIndividualResources( - @NonNull Project project, - @Nullable Project main, - @NonNull List<ResourceXmlDetector> xmlDetectors, - @NonNull List<File> files) { - for (File file : files) { - if (file.isDirectory()) { - // Is it a resource folder? - ResourceFolderType type = ResourceFolderType.getFolderType(file.getName()); - if (type != null && new File(file.getParentFile(), RES_FOLDER).exists()) { - // Yes. - checkResourceFolder(project, main, file, type, xmlDetectors); - } else if (file.getName().equals(RES_FOLDER)) { // Is it the res folder? - // Yes - checkResFolder(project, main, file, xmlDetectors); - } else { - mClient.log(null, "Unexpected folder %1$s; should be project, " + - "\"res\" folder or resource folder", file.getPath()); - continue; - } - } else if (file.isFile() && LintUtils.isXmlFile(file)) { - // Yes, find out its resource type - String folderName = file.getParentFile().getName(); - ResourceFolderType type = ResourceFolderType.getFolderType(folderName); - if (type != null) { - XmlVisitor visitor = getVisitor(type, xmlDetectors); - if (visitor != null) { - XmlContext context = new XmlContext(this, project, main, file, type); - fireEvent(EventType.SCANNING_FILE, context); - visitor.visitFile(context, file); - } - } - } - } - } - - /** - * Adds a listener to be notified of lint progress - * - * @param listener the listener to be added - */ - public void addLintListener(@NonNull LintListener listener) { - if (mListeners == null) { - mListeners = new ArrayList<LintListener>(1); - } - mListeners.add(listener); - } - - /** - * Removes a listener such that it is no longer notified of progress - * - * @param listener the listener to be removed - */ - public void removeLintListener(@NonNull LintListener listener) { - mListeners.remove(listener); - if (mListeners.isEmpty()) { - mListeners = null; - } - } - - /** Notifies listeners, if any, that the given event has occurred */ - private void fireEvent(@NonNull LintListener.EventType type, @Nullable Context context) { - if (mListeners != null) { - for (LintListener listener : mListeners) { - listener.update(this, type, context); - } - } - } - - /** - * Wrapper around the lint client. This sits in the middle between a - * detector calling for example {@link LintClient#report} and - * the actual embedding tool, and performs filtering etc such that detectors - * and lint clients don't have to make sure they check for ignored issues or - * filtered out warnings. - */ - private class LintClientWrapper extends LintClient { - @NonNull - private final LintClient mDelegate; - - public LintClientWrapper(@NonNull LintClient delegate) { - mDelegate = delegate; - } - - @Override - public void report( - @NonNull Context context, - @NonNull Issue issue, - @NonNull Severity severity, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - assert mCurrentProject != null; - if (!mCurrentProject.getReportIssues()) { - return; - } - - Configuration configuration = context.getConfiguration(); - if (!configuration.isEnabled(issue)) { - if (issue != IssueRegistry.PARSER_ERROR && issue != IssueRegistry.LINT_ERROR) { - mDelegate.log(null, "Incorrect detector reported disabled issue %1$s", - issue.toString()); - } - return; - } - - if (configuration.isIgnored(context, issue, location, message, data)) { - return; - } - - if (severity == Severity.IGNORE) { - return; - } - - mDelegate.report(context, issue, severity, location, message, data); - } - - // Everything else just delegates to the embedding lint client - - @Override - @NonNull - public Configuration getConfiguration(@NonNull Project project) { - return mDelegate.getConfiguration(project); - } - - - @Override - public void log(@NonNull Severity severity, @Nullable Throwable exception, - @Nullable String format, @Nullable Object... args) { - mDelegate.log(exception, format, args); - } - - @Override - @NonNull - public String readFile(@NonNull File file) { - return mDelegate.readFile(file); - } - - @Override - @NonNull - public byte[] readBytes(@NonNull File file) throws IOException { - return mDelegate.readBytes(file); - } - - @Override - @NonNull - public List<File> getJavaSourceFolders(@NonNull Project project) { - return mDelegate.getJavaSourceFolders(project); - } - - @Override - @NonNull - public List<File> getJavaClassFolders(@NonNull Project project) { - return mDelegate.getJavaClassFolders(project); - } - - @NonNull - @Override - public List<File> getJavaLibraries(@NonNull Project project) { - return mDelegate.getJavaLibraries(project); - } - - @Override - @NonNull - public List<File> getResourceFolders(@NonNull Project project) { - return mDelegate.getResourceFolders(project); - } - - @Override - @Nullable - public IDomParser getDomParser() { - return mDelegate.getDomParser(); - } - - @Override - @NonNull - public Class<? extends Detector> replaceDetector( - @NonNull Class<? extends Detector> detectorClass) { - return mDelegate.replaceDetector(detectorClass); - } - - @Override - @NonNull - public SdkInfo getSdkInfo(@NonNull Project project) { - return mDelegate.getSdkInfo(project); - } - - @Override - @NonNull - public Project getProject(@NonNull File dir, @NonNull File referenceDir) { - return mDelegate.getProject(dir, referenceDir); - } - - @Override - @Nullable - public IJavaParser getJavaParser() { - return mDelegate.getJavaParser(); - } - - @Override - public File findResource(@NonNull String relativePath) { - return mDelegate.findResource(relativePath); - } - - @Override - @Nullable - public File getCacheDir(boolean create) { - return mDelegate.getCacheDir(create); - } - - @Override - @NonNull - protected ClassPathInfo getClassPath(@NonNull Project project) { - return mDelegate.getClassPath(project); - } - - @Override - public void log(@Nullable Throwable exception, @Nullable String format, - @Nullable Object... args) { - mDelegate.log(exception, format, args); - } - - @Override - @Nullable - public File getSdkHome() { - return mDelegate.getSdkHome(); - } - - @Override - @NonNull - public IAndroidTarget[] getTargets() { - return mDelegate.getTargets(); - } - - @Override - public int getHighestKnownApiLevel() { - return mDelegate.getHighestKnownApiLevel(); - } - - @Override - @Nullable - public String getSuperClass(@NonNull Project project, @NonNull String name) { - return mDelegate.getSuperClass(project, name); - } - - @Override - @Nullable - public Boolean isSubclassOf(@NonNull Project project, @NonNull String name, - @NonNull String superClassName) { - return mDelegate.isSubclassOf(project, name, superClassName); - } - - @Override - @NonNull - public String getProjectName(@NonNull Project project) { - return mDelegate.getProjectName(project); - } - } - - /** - * Requests another pass through the data for the given detector. This is - * typically done when a detector needs to do more expensive computation, - * but it only wants to do this once it <b>knows</b> that an error is - * present, or once it knows more specifically what to check for. - * - * @param detector the detector that should be included in the next pass. - * Note that the lint runner may refuse to run more than a couple - * of runs. - * @param scope the scope to be revisited. This must be a subset of the - * current scope ({@link #getScope()}, and it is just a performance hint; - * in particular, the detector should be prepared to be called on other - * scopes as well (since they may have been requested by other detectors). - * You can pall null to indicate "all". - */ - public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) { - if (mRepeatingDetectors == null) { - mRepeatingDetectors = new ArrayList<Detector>(); - } - mRepeatingDetectors.add(detector); - - if (scope != null) { - if (mRepeatScope == null) { - mRepeatScope = scope; - } else { - mRepeatScope = EnumSet.copyOf(mRepeatScope); - mRepeatScope.addAll(scope); - } - } else { - mRepeatScope = Scope.ALL; - } - } - - // Unfortunately, ASMs nodes do not extend a common DOM node type with parent - // pointers, so we have to have multiple methods which pass in each type - // of node (class, method, field) to be checked. - - /** - * Returns whether the given issue is suppressed in the given method. - * - * @param issue the issue to be checked, or null to just check for "all" - * @param classNode the class containing the issue - * @param method the method containing the issue - * @param instruction the instruction within the method, if any - * @return true if there is a suppress annotation covering the specific - * issue on this method - */ - public boolean isSuppressed( - @Nullable Issue issue, - @NonNull ClassNode classNode, - @NonNull MethodNode method, - @Nullable AbstractInsnNode instruction) { - if (method.invisibleAnnotations != null) { - @SuppressWarnings("unchecked") - List<AnnotationNode> annotations = method.invisibleAnnotations; - return isSuppressed(issue, annotations); - } - - // Initializations of fields end up placed in generated methods (<init> - // for members and <clinit> for static fields). - if (instruction != null && method.name.charAt(0) == '<') { - AbstractInsnNode next = LintUtils.getNextInstruction(instruction); - if (next != null && next.getType() == AbstractInsnNode.FIELD_INSN) { - FieldInsnNode fieldRef = (FieldInsnNode) next; - FieldNode field = findField(classNode, fieldRef.owner, fieldRef.name); - if (field != null && isSuppressed(issue, field)) { - return true; - } - } - } - - return false; - } - - @Nullable - private FieldNode findField(ClassNode classNode, String owner, String name) { - while (classNode != null) { - if (owner.equals(classNode.name)) { - @SuppressWarnings("rawtypes") // ASM API - List fieldList = classNode.fields; - for (Object f : fieldList) { - FieldNode field = (FieldNode) f; - if (field.name.equals(name)) { - return field; - } - } - return null; - } - classNode = getOuterClassNode(classNode); - } - return null; - } - - /** - * Returns whether the given issue is suppressed for the given field. - * - * @param issue the issue to be checked, or null to just check for "all" - * @param field the field potentially annotated with a suppress annotation - * @return true if there is a suppress annotation covering the specific - * issue on this field - */ - public boolean isSuppressed(@Nullable Issue issue, @NonNull FieldNode field) { - if (field.invisibleAnnotations != null) { - @SuppressWarnings("unchecked") - List<AnnotationNode> annotations = field.invisibleAnnotations; - return isSuppressed(issue, annotations); - } - - return false; - } - - /** - * Returns whether the given issue is suppressed in the given class. - * - * @param issue the issue to be checked, or null to just check for "all" - * @param classNode the class containing the issue - * @return true if there is a suppress annotation covering the specific - * issue in this class - */ - public boolean isSuppressed(@Nullable Issue issue, @NonNull ClassNode classNode) { - if (classNode.invisibleAnnotations != null) { - @SuppressWarnings("unchecked") - List<AnnotationNode> annotations = classNode.invisibleAnnotations; - return isSuppressed(issue, annotations); - } - - return false; - } - - private static boolean isSuppressed(@Nullable Issue issue, List<AnnotationNode> annotations) { - for (AnnotationNode annotation : annotations) { - String desc = annotation.desc; - - // We could obey @SuppressWarnings("all") too, but no need to look for it - // because that annotation only has source retention. - - if (desc.endsWith(SUPPRESS_LINT_VMSIG)) { - if (annotation.values != null) { - for (int i = 0, n = annotation.values.size(); i < n; i += 2) { - String key = (String) annotation.values.get(i); - if (key.equals("value")) { //$NON-NLS-1$ - Object value = annotation.values.get(i + 1); - if (value instanceof String) { - String id = (String) value; - if (id.equalsIgnoreCase(SUPPRESS_ALL) || - issue != null && id.equalsIgnoreCase(issue.getId())) { - return true; - } - } else if (value instanceof List) { - @SuppressWarnings("rawtypes") - List list = (List) value; - for (Object v : list) { - if (v instanceof String) { - String id = (String) v; - if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null - && id.equalsIgnoreCase(issue.getId()))) { - return true; - } - } - } - } - } - } - } - } - } - - return false; - } - - /** - * Returns whether the given issue is suppressed in the given parse tree node. - * - * @param issue the issue to be checked, or null to just check for "all" - * @param scope the AST node containing the issue - * @return true if there is a suppress annotation covering the specific - * issue in this class - */ - public boolean isSuppressed(@NonNull Issue issue, @Nullable Node scope) { - while (scope != null) { - Class<? extends Node> type = scope.getClass(); - // The Lombok AST uses a flat hierarchy of node type implementation classes - // so no need to do instanceof stuff here. - if (type == VariableDefinition.class) { - // Variable - VariableDefinition declaration = (VariableDefinition) scope; - if (isSuppressed(issue, declaration.astModifiers())) { - return true; - } - } else if (type == MethodDeclaration.class) { - // Method - // Look for annotations on the method - MethodDeclaration declaration = (MethodDeclaration) scope; - if (isSuppressed(issue, declaration.astModifiers())) { - return true; - } - } else if (type == ConstructorDeclaration.class) { - // Constructor - // Look for annotations on the method - ConstructorDeclaration declaration = (ConstructorDeclaration) scope; - if (isSuppressed(issue, declaration.astModifiers())) { - return true; - } - } else if (type == ClassDeclaration.class) { - // Class - ClassDeclaration declaration = (ClassDeclaration) scope; - if (isSuppressed(issue, declaration.astModifiers())) { - return true; - } - } - - scope = scope.getParent(); - } - - return false; - } - - /** - * Returns true if the given AST modifier has a suppress annotation for the - * given issue (which can be null to check for the "all" annotation) - * - * @param issue the issue to be checked - * @param modifiers the modifier to check - * @return true if the issue or all issues should be suppressed for this - * modifier - */ - private static boolean isSuppressed(@Nullable Issue issue, @Nullable Modifiers modifiers) { - if (modifiers == null) { - return false; - } - StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations(); - if (annotations == null) { - return false; - } - - Iterator<Annotation> iterator = annotations.iterator(); - while (iterator.hasNext()) { - Annotation annotation = iterator.next(); - TypeReference t = annotation.astAnnotationTypeReference(); - String typeName = t.getTypeName(); - if (typeName.endsWith(SUPPRESS_LINT) - || typeName.endsWith("SuppressWarnings")) { //$NON-NLS-1$ - StrictListAccessor<AnnotationElement, Annotation> values = - annotation.astElements(); - if (values != null) { - Iterator<AnnotationElement> valueIterator = values.iterator(); - while (valueIterator.hasNext()) { - AnnotationElement element = valueIterator.next(); - AnnotationValue valueNode = element.astValue(); - if (valueNode == null) { - continue; - } - if (valueNode instanceof StringLiteral) { - StringLiteral literal = (StringLiteral) valueNode; - String value = literal.astValue(); - if (value.equalsIgnoreCase(SUPPRESS_ALL) || - issue != null && issue.getId().equalsIgnoreCase(value)) { - return true; - } - } else if (valueNode instanceof ArrayInitializer) { - ArrayInitializer array = (ArrayInitializer) valueNode; - StrictListAccessor<Expression, ArrayInitializer> expressions = - array.astExpressions(); - if (expressions == null) { - continue; - } - Iterator<Expression> arrayIterator = expressions.iterator(); - while (arrayIterator.hasNext()) { - Expression arrayElement = arrayIterator.next(); - if (arrayElement instanceof StringLiteral) { - String value = ((StringLiteral) arrayElement).astValue(); - if (value.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null - && issue.getId().equalsIgnoreCase(value))) { - return true; - } - } - } - } - } - } - } - } - - return false; - } - - /** - * Returns whether the given issue is suppressed in the given XML DOM node. - * - * @param issue the issue to be checked, or null to just check for "all" - * @param node the DOM node containing the issue - * @return true if there is a suppress annotation covering the specific - * issue in this class - */ - public boolean isSuppressed(@NonNull Issue issue, @Nullable org.w3c.dom.Node node) { - if (node instanceof Attr) { - node = ((Attr) node).getOwnerElement(); - } - while (node != null) { - if (node.getNodeType() == org.w3c.dom.Node.ELEMENT_NODE) { - Element element = (Element) node; - if (element.hasAttributeNS(TOOLS_URI, ATTR_IGNORE)) { - String ignore = element.getAttributeNS(TOOLS_URI, ATTR_IGNORE); - if (ignore.indexOf(',') == -1) { - if (ignore.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null - && issue.getId().equalsIgnoreCase(ignore))) { - return true; - } - } else { - for (String id : ignore.split(",")) { //$NON-NLS-1$ - if (id.equalsIgnoreCase(SUPPRESS_ALL) || (issue != null - && issue.getId().equalsIgnoreCase(id))) { - return true; - } - } - } - } - } - - node = node.getParentNode(); - } - - return false; - } - - /** A pending class to be analyzed by {@link #checkClasses} */ - @VisibleForTesting - static class ClassEntry implements Comparable<ClassEntry> { - public final File file; - public final File jarFile; - public final File binDir; - public final byte[] bytes; - - public ClassEntry(File file, File jarFile, File binDir, byte[] bytes) { - super(); - this.file = file; - this.jarFile = jarFile; - this.binDir = binDir; - this.bytes = bytes; - } - - public String path() { - if (jarFile != null) { - return jarFile.getPath() + ':' + file.getPath(); - } else { - return file.getPath(); - } - } - - @Override - public int compareTo(ClassEntry other) { - String p1 = file.getPath(); - String p2 = other.file.getPath(); - int m1 = p1.length(); - int m2 = p2.length(); - int m = Math.min(m1, m2); - - for (int i = 0; i < m; i++) { - char c1 = p1.charAt(i); - char c2 = p2.charAt(i); - if (c1 != c2) { - // Sort Foo$Bar.class *after* Foo.class, even though $ < . - if (c1 == '.' && c2 == '$') { - return -1; - } - if (c1 == '$' && c2 == '.') { - return 1; - } - return c1 - c2; - } - } - - return (m == m1) ? -1 : 1; - } - - @Override - public String toString() { - return file.getPath(); - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintListener.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintListener.java deleted file mode 100644 index 8195d36..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/LintListener.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.google.common.annotations.Beta; - -/** - * Interface implemented by listeners to be notified of lint events - * <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 interface LintListener { - /** The various types of events provided to lint listeners */ - enum EventType { - /** A lint check is about to begin */ - STARTING, - - /** Lint is about to check the given project, see {@link Context#getProject()} */ - SCANNING_PROJECT, - - /** Lint is about to check the given library project, see {@link Context#getProject()} */ - SCANNING_LIBRARY_PROJECT, - - /** Lint is about to check the given file, see {@link Context#file} */ - SCANNING_FILE, - - /** A new pass was initiated */ - NEW_PHASE, - - /** The lint check was canceled */ - CANCELED, - - /** The lint check is done */ - COMPLETED, - } - - /** - * Notifies listeners that the event of the given type has occurred. - * Additional information, such as the file being scanned, or the project - * being scanned, is available in the {@link Context} object (except for the - * {@link EventType#STARTING}, {@link EventType#CANCELED} or - * {@link EventType#COMPLETED} events which are fired outside of project - * contexts.) - * - * @param driver the driver running through the checks - * @param type the type of event that occurred - * @param context the context providing additional information - */ - void update(@NonNull LintDriver driver, @NonNull EventType type, - @Nullable Context context); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/OtherFileVisitor.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/OtherFileVisitor.java deleted file mode 100644 index 573d4f0..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/OtherFileVisitor.java +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (C) 2013 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.client.api; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.DOT_CLASS; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.FD_ASSETS; -import static com.android.tools.lint.detector.api.Detector.OtherFileScanner; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.Scope; -import com.android.utils.SdkUtils; -import com.google.common.collect.Lists; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.EnumMap; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; - -/** - * Visitor for "other" files: files that aren't java sources, - * XML sources, etc -- or which should have custom handling in some - * other way. - */ -class OtherFileVisitor { - @NonNull - private final List<Detector> mDetectors; - - @NonNull - private Map<Scope, List<File>> mFiles = new EnumMap<Scope, List<File>>(Scope.class); - - OtherFileVisitor(@NonNull List<Detector> detectors) { - mDetectors = detectors; - } - - /** Analyze other files in the given project */ - void scan( - @NonNull LintDriver driver, - @NonNull Project project, - @Nullable Project main) { - // Collect all project files - File projectFolder = project.getDir(); - - EnumSet<Scope> scopes = EnumSet.noneOf(Scope.class); - for (Detector detector : mDetectors) { - OtherFileScanner fileScanner = (OtherFileScanner) detector; - EnumSet<Scope> applicable = fileScanner.getApplicableFiles(); - if (applicable.contains(Scope.OTHER)) { - scopes = Scope.ALL; - break; - } - scopes.addAll(applicable); - } - - List<File> subset = project.getSubset(); - - if (scopes.contains(Scope.RESOURCE_FILE)) { - if (subset != null && !subset.isEmpty()) { - List<File> files = new ArrayList<File>(subset.size()); - for (File file : subset) { - if (SdkUtils.endsWith(file.getPath(), DOT_XML) && - !file.getName().equals(ANDROID_MANIFEST_XML)) { - files.add(file); - } - } - if (!files.isEmpty()) { - mFiles.put(Scope.RESOURCE_FILE, files); - } - } else { - List<File> files = Lists.newArrayListWithExpectedSize(100); - for (File res : project.getResourceFolders()) { - collectFiles(files, res); - } - File assets = new File(projectFolder, FD_ASSETS); - if (assets.exists()) { - collectFiles(files, assets); - } - if (!files.isEmpty()) { - mFiles.put(Scope.RESOURCE_FILE, files); - } - } - } - - if (scopes.contains(Scope.JAVA_FILE)) { - if (subset != null && !subset.isEmpty()) { - List<File> files = new ArrayList<File>(subset.size()); - for (File file : subset) { - if (file.getPath().endsWith(DOT_JAVA)) { - files.add(file); - } - } - if (!files.isEmpty()) { - mFiles.put(Scope.JAVA_FILE, files); - } - } else { - List<File> files = Lists.newArrayListWithExpectedSize(100); - for (File srcFolder : project.getJavaSourceFolders()) { - collectFiles(files, srcFolder); - } - if (!files.isEmpty()) { - mFiles.put(Scope.JAVA_FILE, files); - } - } - } - - if (scopes.contains(Scope.CLASS_FILE)) { - if (subset != null && !subset.isEmpty()) { - List<File> files = new ArrayList<File>(subset.size()); - for (File file : subset) { - if (file.getPath().endsWith(DOT_CLASS)) { - files.add(file); - } - } - if (!files.isEmpty()) { - mFiles.put(Scope.CLASS_FILE, files); - } - } else { - List<File> files = Lists.newArrayListWithExpectedSize(100); - for (File classFolder : project.getJavaClassFolders()) { - collectFiles(files, classFolder); - } - if (!files.isEmpty()) { - mFiles.put(Scope.CLASS_FILE, files); - } - } - } - - if (scopes.contains(Scope.MANIFEST)) { - if (subset != null && !subset.isEmpty()) { - List<File> files = new ArrayList<File>(subset.size()); - for (File file : subset) { - if (file.getName().equals(ANDROID_MANIFEST_XML)) { - files.add(file); - } - } - if (!files.isEmpty()) { - mFiles.put(Scope.MANIFEST, files); - } - } else { - File manifestFile = project.getManifestFile(); - if (manifestFile != null) { - mFiles.put(Scope.MANIFEST, Collections.<File>singletonList(manifestFile)); - } - } - } - - for (Map.Entry<Scope, List<File>> entry : mFiles.entrySet()) { - Scope scope = entry.getKey(); - List<File> files = entry.getValue(); - List<Detector> applicable = new ArrayList<Detector>(mDetectors.size()); - for (Detector detector : mDetectors) { - OtherFileScanner fileScanner = (OtherFileScanner) detector; - EnumSet<Scope> appliesTo = fileScanner.getApplicableFiles(); - if (appliesTo.contains(Scope.OTHER) || appliesTo.contains(scope)) { - applicable.add(detector); - } - } - if (!applicable.isEmpty()) { - for (File file : files) { - Context context = new Context(driver, project, main, file); - for (Detector detector : applicable) { - detector.beforeCheckFile(context); - detector.run(context); - detector.afterCheckFile(context); - } - if (driver.isCanceled()) { - return; - } - } - } - } - } - - private static void collectFiles(List<File> files, File file) { - if (file.isDirectory()) { - File[] children = file.listFiles(); - if (children != null) { - for (File child : children) { - collectFiles(files, child); - } - } - } else { - files.add(file); - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/SdkInfo.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/SdkInfo.java deleted file mode 100644 index 8b3d1e9..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/SdkInfo.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.annotations.Beta; - -/** - * Information about SDKs - * <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 SdkInfo { - /** - * Returns true if the given child view is the same class or a sub class of - * the given parent view class - * - * @param parentViewFqcn the fully qualified class name of the parent view - * @param childViewFqcn the fully qualified class name of the child view - * @return true if the child view is a sub view of (or the same class as) - * the parent view - */ - public boolean isSubViewOf(@NonNull String parentViewFqcn, @NonNull String childViewFqcn) { - while (!childViewFqcn.equals("android.view.View")) { //$NON-NLS-1$ - if (parentViewFqcn.equals(childViewFqcn)) { - return true; - } - String parent = getParentViewClass(childViewFqcn); - if (parent == null) { - // Unknown view - err on the side of caution - return true; - } - childViewFqcn = parent; - } - - return false; - } - - - /** - * Returns the fully qualified name of the parent view, or null if the view - * is the root android.view.View class. - * - * @param fqcn the fully qualified class name of the view - * @return the fully qualified class name of the parent view, or null - */ - @Nullable - public abstract String getParentViewClass(@NonNull String fqcn); - - /** - * Returns the class name of the parent view, or null if the view is the - * root android.view.View class. This is the same as the - * {@link #getParentViewClass(String)} but without the package. - * - * @param name the view class name to look up the parent for (not including - * package) - * @return the view name of the parent - */ - @Nullable - public abstract String getParentViewName(@NonNull String name); - - /** - * Returns true if the given widget name is a layout - * - * @param tag the XML tag for the view - * @return true if the given tag corresponds to a layout - */ - public boolean isLayout(@NonNull String tag) { - return tag.endsWith("Layout"); //$NON-NLS-1$ - } - - // TODO: Add access to resource resolution here. -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/XmlVisitor.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/XmlVisitor.java deleted file mode 100644 index 2e64118..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/client/api/XmlVisitor.java +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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.client.api; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.XmlScanner; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.XmlContext; -import com.google.common.annotations.Beta; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.RandomAccess; - -/** - * Specialized visitor for running detectors on an XML document. - * It operates in two phases: - * <ol> - * <li> First, it computes a set of maps where it generates a map from each - * significant element name, and each significant attribute name, to a list - * of detectors to consult for that element or attribute name. - * The set of element names or attribute names (or both) that a detector - * is interested in is provided by the detectors themselves. - * <li> Second, it iterates over the document a single time. For each element and - * attribute it looks up the list of interested detectors, and runs them. - * </ol> - * It also notifies all the detectors before and after the document is processed - * such that they can do pre- and post-processing. - * <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 -class XmlVisitor { - private final Map<String, List<Detector.XmlScanner>> mElementToCheck = - new HashMap<String, List<Detector.XmlScanner>>(); - private final Map<String, List<Detector.XmlScanner>> mAttributeToCheck = - new HashMap<String, List<Detector.XmlScanner>>(); - private final List<Detector.XmlScanner> mDocumentDetectors = - new ArrayList<Detector.XmlScanner>(); - private final List<Detector.XmlScanner> mAllElementDetectors = - new ArrayList<Detector.XmlScanner>(); - private final List<Detector.XmlScanner> mAllAttributeDetectors = - new ArrayList<Detector.XmlScanner>(); - private final List<? extends Detector> mAllDetectors; - private final IDomParser mParser; - - // Really want this: - //<T extends List<Detector> & Detector.XmlScanner> XmlVisitor(IDomParser parser, - // T xmlDetectors) { - // but it makes client code tricky and ugly. - XmlVisitor(@NonNull IDomParser parser, @NonNull List<? extends Detector> xmlDetectors) { - mParser = parser; - mAllDetectors = xmlDetectors; - - // TODO: Check appliesTo() for files, and find a quick way to enable/disable - // rules when running through a full project! - for (Detector detector : xmlDetectors) { - Detector.XmlScanner xmlDetector = (XmlScanner) detector; - Collection<String> attributes = xmlDetector.getApplicableAttributes(); - if (attributes == XmlScanner.ALL) { - mAllAttributeDetectors.add(xmlDetector); - } else if (attributes != null) { - for (String attribute : attributes) { - List<Detector.XmlScanner> list = mAttributeToCheck.get(attribute); - if (list == null) { - list = new ArrayList<Detector.XmlScanner>(); - mAttributeToCheck.put(attribute, list); - } - list.add(xmlDetector); - } - } - Collection<String> elements = xmlDetector.getApplicableElements(); - if (elements == XmlScanner.ALL) { - mAllElementDetectors.add(xmlDetector); - } else if (elements != null) { - for (String element : elements) { - List<Detector.XmlScanner> list = mElementToCheck.get(element); - if (list == null) { - list = new ArrayList<Detector.XmlScanner>(); - mElementToCheck.put(element, list); - } - list.add(xmlDetector); - } - } - - if ((attributes == null || (attributes.isEmpty() - && attributes != XmlScanner.ALL)) - && (elements == null || (elements.isEmpty() - && elements != XmlScanner.ALL))) { - mDocumentDetectors.add(xmlDetector); - } - } - } - - void visitFile(@NonNull XmlContext context, @NonNull File file) { - assert LintUtils.isXmlFile(file); - context.parser = mParser; - - try { - if (context.document == null) { - context.document = mParser.parseXml(context); - if (context.document == null) { - // No need to log this; the parser should be reporting - // a full warning (such as IssueRegistry#PARSER_ERROR) - // with details, location, etc. - return; - } - if (context.document.getDocumentElement() == null) { - // Ignore empty documents - return; - } - } - - for (Detector check : mAllDetectors) { - check.beforeCheckFile(context); - } - - for (Detector.XmlScanner check : mDocumentDetectors) { - check.visitDocument(context, context.document); - } - - if (!mElementToCheck.isEmpty() || !mAttributeToCheck.isEmpty() - || !mAllAttributeDetectors.isEmpty() || !mAllElementDetectors.isEmpty()) { - visitElement(context, context.document.getDocumentElement()); - } - - for (Detector check : mAllDetectors) { - check.afterCheckFile(context); - } - } finally { - if (context.document != null) { - mParser.dispose(context, context.document); - context.document = null; - } - } - } - - private void visitElement(@NonNull XmlContext context, @NonNull Element element) { - List<Detector.XmlScanner> elementChecks = mElementToCheck.get(element.getTagName()); - if (elementChecks != null) { - assert elementChecks instanceof RandomAccess; - for (XmlScanner check : elementChecks) { - check.visitElement(context, element); - } - } - if (!mAllElementDetectors.isEmpty()) { - for (XmlScanner check : mAllElementDetectors) { - check.visitElement(context, element); - } - } - - if (!mAttributeToCheck.isEmpty() || !mAllAttributeDetectors.isEmpty()) { - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attribute = (Attr) attributes.item(i); - String name = attribute.getLocalName(); - if (name == null) { - name = attribute.getName(); - } - List<Detector.XmlScanner> list = mAttributeToCheck.get(name); - if (list != null) { - for (XmlScanner check : list) { - check.visitAttribute(context, attribute); - } - } - if (!mAllAttributeDetectors.isEmpty()) { - for (XmlScanner check : mAllAttributeDetectors) { - check.visitAttribute(context, attribute); - } - } - } - } - - // Visit children - NodeList childNodes = element.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - visitElement(context, (Element) child); - } - } - - // Post hooks - if (elementChecks != null) { - for (XmlScanner check : elementChecks) { - check.visitElementAfter(context, element); - } - } - if (!mAllElementDetectors.isEmpty()) { - for (XmlScanner check : mAllElementDetectors) { - check.visitElementAfter(context, element); - } - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Category.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Category.java deleted file mode 100644 index c267420..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Category.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.annotations.Beta; - -/** - * A category is a container for related issues. - * <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 final class Category implements Comparable<Category> { - private final String mName; - private final int mPriority; - private final Category mParent; - - /** - * Creates a new {@link Category}. - * - * @param parent the name of a parent category, or null - * @param name the name of the category - * @param priority a sorting priority, with higher being more important - */ - private Category( - @Nullable Category parent, - @NonNull String name, - int priority) { - mParent = parent; - mName = name; - mPriority = priority; - } - - /** - * Creates a new top level {@link Category} with the given sorting priority. - * - * @param name the name of the category - * @param priority a sorting priority, with higher being more important - * @return a new category - */ - @NonNull - public static Category create(@NonNull String name, int priority) { - return new Category(null, name, priority); - } - - /** - * Creates a new top level {@link Category} with the given sorting priority. - * - * @param parent the name of a parent category, or null - * @param name the name of the category - * @param priority a sorting priority, with higher being more important - * @return a new category - */ - @NonNull - public static Category create(@Nullable Category parent, @NonNull String name, int priority) { - return new Category(parent, name, priority); - } - - /** - * Returns the parent category, or null if this is a top level category - * - * @return the parent category, or null if this is a top level category - */ - public Category getParent() { - return mParent; - } - - /** - * Returns the name of this category - * - * @return the name of this category - */ - public String getName() { - return mName; - } - - /** - * Returns a full name for this category. For a top level category, this is just - * the {@link #getName()} value, but for nested categories it will include the parent - * names as well. - * - * @return a full name for this category - */ - public String getFullName() { - if (mParent != null) { - return mParent.getFullName() + ':' + mName; - } else { - return mName; - } - } - - @Override - public int compareTo(Category other) { - if (other.mPriority == mPriority) { - if (mParent == other) { - return 1; - } else if (other.mParent == this) { - return -1; - } - } - return other.mPriority - mPriority; - } - - /** Issues related to running lint itself */ - public static final Category LINT = create("Lint", 110); - - /** Issues related to correctness */ - public static final Category CORRECTNESS = create("Correctness", 100); - - /** Issues related to security */ - public static final Category SECURITY = create("Security", 90); - - /** Issues related to performance */ - public static final Category PERFORMANCE = create("Performance", 80); - - /** Issues related to usability */ - public static final Category USABILITY = create("Usability", 70); - - /** Issues related to accessibility */ - public static final Category A11Y = create("Accessibility", 60); - - /** Issues related to internationalization */ - public static final Category I18N = create("Internationalization", 50); - - // Sub categories - - /** Issues related to icons */ - public static final Category ICONS = create(USABILITY, "Icons", 73); - - /** Issues related to typography */ - public static final Category TYPOGRAPHY = create(USABILITY, "Typography", 76); - - /** Issues related to messages/strings */ - public static final Category MESSAGES = create(CORRECTNESS, "Messages", 95); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java deleted file mode 100644 index 5150eff..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ClassContext.java +++ /dev/null @@ -1,682 +0,0 @@ -/* - * 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.detector.api; - -import static com.android.SdkConstants.CONSTRUCTOR_NAME; -import static com.android.SdkConstants.DOT_CLASS; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD; -import static com.android.tools.lint.detector.api.Location.SearchDirection.EOL_BACKWARD; -import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Location.SearchDirection; -import com.android.tools.lint.detector.api.Location.SearchHints; -import com.google.common.annotations.Beta; -import com.google.common.base.Splitter; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldNode; -import org.objectweb.asm.tree.LineNumberNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.io.File; -import java.util.List; - -/** - * A {@link Context} used when checking .class files. - * <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 class ClassContext extends Context { - private final File mBinDir; - /** The class file DOM root node */ - private final ClassNode mClassNode; - /** The class file byte data */ - private final byte[] mBytes; - /** The source file, if known/found */ - private File mSourceFile; - /** The contents of the source file, if source file is known/found */ - private String mSourceContents; - /** Whether we've searched for the source file (used to avoid repeated failed searches) */ - private boolean mSearchedForSource; - /** If the file is a relative path within a jar file, this is the jar file, otherwise null */ - private final File mJarFile; - /** Whether this class is part of a library (rather than corresponding to one of the - * source files in this project */ - private final boolean mFromLibrary; - - /** - * Construct a new {@link ClassContext} - * - * @param driver the driver running through the checks - * @param project the project containing the file being checked - * @param main the main project if this project is a library project, or - * null if this is not a library project. The main project is the - * root project of all library projects, not necessarily the - * directly including project. - * @param file the file being checked - * @param jarFile If the file is a relative path within a jar file, this is - * the jar file, otherwise null - * @param binDir the root binary directory containing this .class file. - * @param bytes the bytecode raw data - * @param classNode the bytecode object model - * @param fromLibrary whether this class is from a library rather than part - * of this project - * @param sourceContents initial contents of the Java source, if known, or - * null - */ - public ClassContext( - @NonNull LintDriver driver, - @NonNull Project project, - @Nullable Project main, - @NonNull File file, - @Nullable File jarFile, - @NonNull File binDir, - @NonNull byte[] bytes, - @NonNull ClassNode classNode, - boolean fromLibrary, - @Nullable String sourceContents) { - super(driver, project, main, file); - mJarFile = jarFile; - mBinDir = binDir; - mBytes = bytes; - mClassNode = classNode; - mFromLibrary = fromLibrary; - mSourceContents = sourceContents; - } - - /** - * Returns the raw bytecode data for this class file - * - * @return the byte array containing the bytecode data - */ - @NonNull - public byte[] getBytecode() { - return mBytes; - } - - /** - * Returns the bytecode object model - * - * @return the bytecode object model, never null - */ - @NonNull - public ClassNode getClassNode() { - return mClassNode; - } - - /** - * Returns the jar file, if any. If this is null, the .class file is a real file - * on disk, otherwise it represents a relative path within the jar file. - * - * @return the jar file, or null - */ - @Nullable - public File getJarFile() { - return mJarFile; - } - - /** - * Returns whether this class is part of a library (not this project). - * - * @return true if this class is part of a library - */ - public boolean isFromClassLibrary() { - return mFromLibrary; - } - - /** - * Returns the source file for this class file, if possible. - * - * @return the source file, or null - */ - @Nullable - public File getSourceFile() { - if (mSourceFile == null && !mSearchedForSource) { - mSearchedForSource = true; - - String source = mClassNode.sourceFile; - if (source == null) { - source = file.getName(); - if (source.endsWith(DOT_CLASS)) { - source = source.substring(0, source.length() - DOT_CLASS.length()) + DOT_JAVA; - } - int index = source.indexOf('$'); - if (index != -1) { - source = source.substring(0, index) + DOT_JAVA; - } - } - if (source != null) { - if (mJarFile != null) { - String relative = file.getParent() + File.separator + source; - List<File> sources = getProject().getJavaSourceFolders(); - for (File dir : sources) { - File sourceFile = new File(dir, relative); - if (sourceFile.exists()) { - mSourceFile = sourceFile; - break; - } - } - } else { - // Determine package - String topPath = mBinDir.getPath(); - String parentPath = file.getParentFile().getPath(); - if (parentPath.startsWith(topPath)) { - int start = topPath.length() + 1; - String relative = start > parentPath.length() ? // default package? - "" : parentPath.substring(start); - List<File> sources = getProject().getJavaSourceFolders(); - for (File dir : sources) { - File sourceFile = new File(dir, relative + File.separator + source); - if (sourceFile.exists()) { - mSourceFile = sourceFile; - break; - } - } - } - } - } - } - - return mSourceFile; - } - - /** - * Returns the contents of the source file for this class file, if found. - * - * @return the source contents, or "" - */ - @NonNull - public String getSourceContents() { - if (mSourceContents == null) { - File sourceFile = getSourceFile(); - if (sourceFile != null) { - mSourceContents = getClient().readFile(mSourceFile); - } - - if (mSourceContents == null) { - mSourceContents = ""; - } - } - - return mSourceContents; - } - - /** - * Returns the contents of the source file for this class file, if found. If - * {@code read} is false, do not read the source contents if it has not - * already been read. (This is primarily intended for the lint - * infrastructure; most client code would call {@link #getSourceContents()} - * .) - * - * @param read whether to read the source contents if it has not already - * been initialized - * @return the source contents, which will never be null if {@code read} is - * true, or null if {@code read} is false and the source contents - * hasn't already been read. - */ - @Nullable - public String getSourceContents(boolean read) { - if (read) { - return getSourceContents(); - } else { - return mSourceContents; - } - } - - /** - * Returns a location for the given source line number in this class file's - * source file, if available. - * - * @param line the line number (1-based, which is what ASM uses) - * @param patternStart optional pattern to search for in the source for - * range start - * @param patternEnd optional pattern to search for in the source for range - * end - * @param hints additional hints about the pattern search (provided - * {@code patternStart} is non null) - * @return a location, never null - */ - @NonNull - public Location getLocationForLine(int line, @Nullable String patternStart, - @Nullable String patternEnd, @Nullable SearchHints hints) { - File sourceFile = getSourceFile(); - if (sourceFile != null) { - // ASM line numbers are 1-based, and lint line numbers are 0-based - if (line != -1) { - return Location.create(sourceFile, getSourceContents(), line - 1, - patternStart, patternEnd, hints); - } else { - return Location.create(sourceFile); - } - } - - return Location.create(file); - } - - /** - * Reports an issue. - * <p> - * Detectors should only call this method if an error applies to the whole class - * scope and there is no specific method or field that applies to the error. - * If so, use - * {@link #report(Issue, MethodNode, AbstractInsnNode, Location, String, Object)} or - * {@link #report(Issue, FieldNode, Location, String, Object)}, such that - * suppress annotations are checked. - * - * @param issue the issue to report - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - @Override - public void report( - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - if (mDriver.isSuppressed(issue, mClassNode)) { - return; - } - ClassNode curr = mClassNode; - while (curr != null) { - ClassNode prev = curr; - curr = mDriver.getOuterClassNode(curr); - if (curr != null) { - if (prev.outerMethod != null) { - @SuppressWarnings("rawtypes") // ASM API - List methods = curr.methods; - for (Object m : methods) { - MethodNode method = (MethodNode) m; - if (method.name.equals(prev.outerMethod) - && method.desc.equals(prev.outerMethodDesc)) { - // Found the outer method for this anonymous class; continue - // reporting on it (which will also work its way up the parent - // class hierarchy) - if (method != null && mDriver.isSuppressed(issue, mClassNode, method, - null)) { - return; - } - break; - } - } - } - if (mDriver.isSuppressed(issue, curr)) { - return; - } - } - } - - super.report(issue, location, message, data); - } - - // Unfortunately, ASMs nodes do not extend a common DOM node type with parent - // pointers, so we have to have multiple methods which pass in each type - // of node (class, method, field) to be checked. - - /** - * Reports an issue applicable to a given method node. - * - * @param issue the issue to report - * @param method the method scope the error applies to. The lint - * infrastructure will check whether there are suppress - * annotations on this method (or its enclosing class) and if so - * suppress the warning without involving the client. - * @param instruction the instruction within the method the error applies - * to. You cannot place annotations on individual method - * instructions (for example, annotations on local variables are - * allowed, but are not kept in the .class file). However, this - * instruction is needed to handle suppressing errors on field - * initializations; in that case, the errors may be reported in - * the {@code <clinit>} method, but the annotation is found not - * on that method but for the {@link FieldNode}'s. - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - public void report( - @NonNull Issue issue, - @Nullable MethodNode method, - @Nullable AbstractInsnNode instruction, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - if (method != null && mDriver.isSuppressed(issue, mClassNode, method, instruction)) { - return; - } - report(issue, location, message, data); // also checks the class node - } - - /** - * Reports an issue applicable to a given method node. - * - * @param issue the issue to report - * @param field the scope the error applies to. The lint infrastructure - * will check whether there are suppress annotations on this field (or its enclosing - * class) and if so suppress the warning without involving the client. - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - public void report( - @NonNull Issue issue, - @Nullable FieldNode field, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - if (field != null && mDriver.isSuppressed(issue, field)) { - return; - } - report(issue, location, message, data); // also checks the class node - } - - /** - * Finds the line number closest to the given node - * - * @param node the instruction node to get a line number for - * @return the closest line number, or -1 if not known - */ - public static int findLineNumber(@NonNull AbstractInsnNode node) { - AbstractInsnNode curr = node; - - // First search backwards - while (curr != null) { - if (curr.getType() == AbstractInsnNode.LINE) { - return ((LineNumberNode) curr).line; - } - curr = curr.getPrevious(); - } - - // Then search forwards - curr = node; - while (curr != null) { - if (curr.getType() == AbstractInsnNode.LINE) { - return ((LineNumberNode) curr).line; - } - curr = curr.getNext(); - } - - return -1; - } - - /** - * Finds the line number closest to the given method declaration - * - * @param node the method node to get a line number for - * @return the closest line number, or -1 if not known - */ - public static int findLineNumber(@NonNull MethodNode node) { - if (node.instructions != null && node.instructions.size() > 0) { - return findLineNumber(node.instructions.get(0)); - } - - return -1; - } - - /** - * Finds the line number closest to the given class declaration - * - * @param node the method node to get a line number for - * @return the closest line number, or -1 if not known - */ - public static int findLineNumber(@NonNull ClassNode node) { - if (node.methods != null && !node.methods.isEmpty()) { - MethodNode firstMethod = getFirstRealMethod(node); - if (firstMethod != null) { - return findLineNumber(firstMethod); - } - } - - return -1; - } - - /** - * Returns a location for the given {@link ClassNode}, where class node is - * either the top level class, or an inner class, in the current context. - * - * @param classNode the class in the current context - * @return a location pointing to the class declaration, or as close to it - * as possible - */ - @NonNull - public Location getLocation(@NonNull ClassNode classNode) { - // Attempt to find a proper location for this class. This is tricky - // since classes do not have line number entries in the class file; we need - // to find a method, look up the corresponding line number then search - // around it for a suitable tag, such as the class name. - String pattern; - if (isAnonymousClass(classNode.name)) { - pattern = classNode.superName; - } else { - pattern = classNode.name; - } - int index = pattern.lastIndexOf('$'); - if (index != -1) { - pattern = pattern.substring(index + 1); - } - index = pattern.lastIndexOf('/'); - if (index != -1) { - pattern = pattern.substring(index + 1); - } - - return getLocationForLine(findLineNumber(classNode), pattern, null, - SearchHints.create(BACKWARD).matchJavaSymbol()); - } - - @Nullable - private static MethodNode getFirstRealMethod(@NonNull ClassNode classNode) { - // Return the first method in the class for line number purposes. Skip <init>, - // since it's typically not located near the real source of the method. - if (classNode.methods != null) { - @SuppressWarnings("rawtypes") // ASM API - List methods = classNode.methods; - for (Object m : methods) { - MethodNode method = (MethodNode) m; - if (method.name.charAt(0) != '<') { - return method; - } - } - - if (!classNode.methods.isEmpty()) { - return (MethodNode) classNode.methods.get(0); - } - } - - return null; - } - - /** - * Returns a location for the given {@link MethodNode}. - * - * @param methodNode the class in the current context - * @param classNode the class containing the method - * @return a location pointing to the class declaration, or as close to it - * as possible - */ - @NonNull - public Location getLocation(@NonNull MethodNode methodNode, - @NonNull ClassNode classNode) { - // Attempt to find a proper location for this class. This is tricky - // since classes do not have line number entries in the class file; we need - // to find a method, look up the corresponding line number then search - // around it for a suitable tag, such as the class name. - String pattern; - SearchDirection searchMode; - if (methodNode.name.equals(CONSTRUCTOR_NAME)) { - searchMode = EOL_BACKWARD; - if (isAnonymousClass(classNode.name)) { - pattern = classNode.superName.substring(classNode.superName.lastIndexOf('/') + 1); - } else { - pattern = classNode.name.substring(classNode.name.lastIndexOf('$') + 1); - } - } else { - searchMode = BACKWARD; - pattern = methodNode.name; - } - - return getLocationForLine(findLineNumber(methodNode), pattern, null, - SearchHints.create(searchMode).matchJavaSymbol()); - } - - /** - * Returns a location for the given {@link AbstractInsnNode}. - * - * @param instruction the instruction to look up the location for - * @return a location pointing to the instruction, or as close to it - * as possible - */ - @NonNull - public Location getLocation(@NonNull AbstractInsnNode instruction) { - SearchHints hints = SearchHints.create(FORWARD).matchJavaSymbol(); - String pattern = null; - if (instruction instanceof MethodInsnNode) { - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.name.equals(CONSTRUCTOR_NAME)) { - pattern = call.owner; - hints = hints.matchConstructor(); - } else { - pattern = call.name; - } - int index = pattern.lastIndexOf('$'); - if (index != -1) { - pattern = pattern.substring(index + 1); - } - index = pattern.lastIndexOf('/'); - if (index != -1) { - pattern = pattern.substring(index + 1); - } - } - - int line = findLineNumber(instruction); - return getLocationForLine(line, pattern, null, hints); - } - - private static boolean isAnonymousClass(@NonNull String fqcn) { - int lastIndex = fqcn.lastIndexOf('$'); - if (lastIndex != -1 && lastIndex < fqcn.length() - 1) { - if (Character.isDigit(fqcn.charAt(lastIndex + 1))) { - return true; - } - } - return false; - } - - /** - * Converts from a VM owner name (such as foo/bar/Foo$Baz) to a - * fully qualified class name (such as foo.bar.Foo.Baz). - * - * @param owner the owner name to convert - * @return the corresponding fully qualified class name - */ - @NonNull - public static String getFqcn(@NonNull String owner) { - return owner.replace('/', '.').replace('$','.'); - } - - /** - * Computes a user-readable type signature from the given class owner, name - * and description. For example, for owner="foo/bar/Foo$Baz", name="foo", - * description="(I)V", it returns "void foo.bar.Foo.Bar#foo(int)". - * - * @param owner the class name - * @param name the method name - * @param desc the method description - * @return a user-readable string - */ - public static String createSignature(String owner, String name, String desc) { - StringBuilder sb = new StringBuilder(100); - - if (desc != null) { - Type returnType = Type.getReturnType(desc); - sb.append(getTypeString(returnType)); - sb.append(' '); - } - - if (owner != null) { - sb.append(getFqcn(owner)); - } - if (name != null) { - sb.append('#'); - sb.append(name); - if (desc != null) { - Type[] argumentTypes = Type.getArgumentTypes(desc); - if (argumentTypes != null && argumentTypes.length > 0) { - sb.append('('); - boolean first = true; - for (Type type : argumentTypes) { - if (first) { - first = false; - } else { - sb.append(", "); - } - sb.append(getTypeString(type)); - } - sb.append(')'); - } - } - } - - return sb.toString(); - } - - private static String getTypeString(Type type) { - String s = type.getClassName(); - if (s.startsWith("java.lang.")) { //$NON-NLS-1$ - s = s.substring("java.lang.".length()); //$NON-NLS-1$ - } - - return s; - } - - /** - * Computes the internal class name of the given fully qualified class name. - * For example, it converts foo.bar.Foo.Bar into foo/bar/Foo$Bar - * - * @param fqcn the fully qualified class name - * @return the internal class name - */ - @NonNull - public static String getInternalName(@NonNull String fqcn) { - if (fqcn.indexOf('.') == -1) { - return fqcn; - } - - StringBuilder sb = new StringBuilder(fqcn.length()); - String prev = null; - for (String part : Splitter.on('.').split(fqcn)) { - if (prev != null && prev.length() > 0) { - if (Character.isUpperCase(prev.charAt(0))) { - sb.append('$'); - } else { - sb.append('/'); - } - } - sb.append(part); - prev = part; - } - - return sb.toString(); - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Context.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Context.java deleted file mode 100644 index a379dd0..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Context.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.Configuration; -import com.android.tools.lint.client.api.LintClient; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.client.api.SdkInfo; -import com.google.common.annotations.Beta; -import com.google.common.base.Splitter; - -import java.io.File; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Context passed to the detectors during an analysis run. It provides - * information about the file being analyzed, it allows shared properties (so - * the detectors can share results), etc. - * <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 class Context { - /** - * The file being checked. Note that this may not always be to a concrete - * file. For example, in the {@link Detector#beforeCheckProject(Context)} - * method, the context file is the directory of the project. - */ - public final File file; - - /** The driver running through the checks */ - protected final LintDriver mDriver; - - /** The project containing the file being checked */ - @NonNull - private final Project mProject; - - /** - * The "main" project. For normal projects, this is the same as {@link #mProject}, - * but for library projects, it's the root project that includes (possibly indirectly) - * the various library projects and their library projects. - * <p> - * Note that this is a property on the {@link Context}, not the - * {@link Project}, since a library project can be included from multiple - * different top level projects, so there isn't <b>one</b> main project, - * just one per main project being analyzed with its library projects. - */ - private final Project mMainProject; - - /** The current configuration controlling which checks are enabled etc */ - private final Configuration mConfiguration; - - /** The contents of the file */ - private String mContents; - - /** - * Whether the lint job has been canceled. - * <p> - * Slow-running detectors should check this flag via - * {@link AtomicBoolean#get()} and abort if canceled - */ - @NonNull - public final AtomicBoolean canceled = new AtomicBoolean(); - - /** Map of properties to share results between detectors */ - private Map<String, Object> mProperties; - - /** - * Construct a new {@link Context} - * - * @param driver the driver running through the checks - * @param project the project containing the file being checked - * @param main the main project if this project is a library project, or - * null if this is not a library project. The main project is - * the root project of all library projects, not necessarily the - * directly including project. - * @param file the file being checked - */ - public Context( - @NonNull LintDriver driver, - @NonNull Project project, - @Nullable Project main, - @NonNull File file) { - this.file = file; - - mDriver = driver; - mProject = project; - mMainProject = main; - mConfiguration = project.getConfiguration(); - } - - /** - * Returns the scope for the lint job - * - * @return the scope, never null - */ - @NonNull - public EnumSet<Scope> getScope() { - return mDriver.getScope(); - } - - /** - * Returns the configuration for this project. - * - * @return the configuration, never null - */ - @NonNull - public Configuration getConfiguration() { - return mConfiguration; - } - - /** - * Returns the project containing the file being checked - * - * @return the project, never null - */ - @NonNull - public Project getProject() { - return mProject; - } - - /** - * Returns the main project if this project is a library project, or self - * if this is not a library project. The main project is the root project - * of all library projects, not necessarily the directly including project. - * - * @return the main project, never null - */ - @NonNull - public Project getMainProject() { - return mMainProject != null ? mMainProject : mProject; - } - - /** - * Returns the lint client requesting the lint check - * - * @return the client, never null - */ - @NonNull - public LintClient getClient() { - return mDriver.getClient(); - } - - /** - * Returns the driver running through the lint checks - * - * @return the driver - */ - @NonNull - public LintDriver getDriver() { - return mDriver; - } - - /** - * Returns the contents of the file. This may not be the contents of the - * file on disk, since it delegates to the {@link LintClient}, which in turn - * may decide to return the current edited contents of the file open in an - * editor. - * - * @return the contents of the given file, or null if an error occurs. - */ - @Nullable - public String getContents() { - if (mContents == null) { - mContents = mDriver.getClient().readFile(file); - } - - return mContents; - } - - /** - * Returns the value of the given named property, or null. - * - * @param name the name of the property - * @return the corresponding value, or null - */ - @Nullable - public Object getProperty(String name) { - if (mProperties == null) { - return null; - } - - return mProperties.get(name); - } - - /** - * Sets the value of the given named property. - * - * @param name the name of the property - * @param value the corresponding value - */ - public void setProperty(@NonNull String name, @Nullable Object value) { - if (value == null) { - if (mProperties != null) { - mProperties.remove(name); - } - } else { - if (mProperties == null) { - mProperties = new HashMap<String, Object>(); - } - mProperties.put(name, value); - } - } - - /** - * Gets the SDK info for the current project. - * - * @return the SDK info for the current project, never null - */ - @NonNull - public SdkInfo getSdkInfo() { - return mProject.getSdkInfo(); - } - - // ---- Convenience wrappers ---- (makes the detector code a bit leaner) - - /** - * Returns false if the given issue has been disabled. Convenience wrapper - * around {@link Configuration#getSeverity(Issue)}. - * - * @param issue the issue to check - * @return false if the issue has been disabled - */ - public boolean isEnabled(@NonNull Issue issue) { - return mConfiguration.isEnabled(issue); - } - - /** - * Reports an issue. Convenience wrapper around {@link LintClient#report} - * - * @param issue the issue to report - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - public void report( - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - Configuration configuration = mConfiguration; - - // If this error was computed for a context where the context corresponds to - // a project instead of a file, the actual error may be in a different project (e.g. - // a library project), so adjust the configuration as necessary. - if (location != null && location.getFile() != null) { - Project project = mDriver.findProjectFor(location.getFile()); - if (project != null) { - configuration = project.getConfiguration(); - } - } - - // If an error occurs in a library project, but you've disabled that check in the - // main project, disable it in the library project too. (In some cases you don't - // control the lint.xml of a library project, and besides, if you're not interested in - // a check for your main project you probably don't care about it in the library either.) - if (configuration != mConfiguration - && mConfiguration.getSeverity(issue) == Severity.IGNORE) { - return; - } - - Severity severity = configuration.getSeverity(issue); - if (severity == Severity.IGNORE) { - return; - } - - mDriver.getClient().report(this, issue, severity, location, message, data); - } - - /** - * Send an exception to the log. Convenience wrapper around {@link LintClient#log}. - * - * @param exception the exception, possibly null - * @param format the error message using {@link String#format} syntax, possibly null - * @param args any arguments for the format string - */ - public void log( - @Nullable Throwable exception, - @Nullable String format, - @Nullable Object... args) { - mDriver.getClient().log(exception, format, args); - } - - /** - * Returns the current phase number. The first pass is numbered 1. Only one pass - * will be performed, unless a {@link Detector} calls {@link #requestRepeat}. - * - * @return the current phase, usually 1 - */ - public int getPhase() { - return mDriver.getPhase(); - } - - /** - * Requests another pass through the data for the given detector. This is - * typically done when a detector needs to do more expensive computation, - * but it only wants to do this once it <b>knows</b> that an error is - * present, or once it knows more specifically what to check for. - * - * @param detector the detector that should be included in the next pass. - * Note that the lint runner may refuse to run more than a couple - * of runs. - * @param scope the scope to be revisited. This must be a subset of the - * current scope ({@link #getScope()}, and it is just a performance hint; - * in particular, the detector should be prepared to be called on other - * scopes as well (since they may have been requested by other detectors). - * You can pall null to indicate "all". - */ - public void requestRepeat(@NonNull Detector detector, @Nullable EnumSet<Scope> scope) { - mDriver.requestRepeat(detector, scope); - } - - /** Pattern for version qualifiers */ - private static final Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)$"); //$NON-NLS-1$ - - private static File sCachedFolder = null; - private static int sCachedFolderVersion = -1; - - /** - * Returns the folder version. For example, for the file values-v14/foo.xml, - * it returns 14. - * - * @return the folder version, or -1 if no specific version was specified - */ - public int getFolderVersion() { - return getFolderVersion(file); - } - - /** - * Returns the folder version of the given file. For example, for the file values-v14/foo.xml, - * it returns 14. - * - * @param file the file to be checked - * @return the folder version, or -1 if no specific version was specified - */ - public static int getFolderVersion(File file) { - File parent = file.getParentFile(); - if (parent.equals(sCachedFolder)) { - return sCachedFolderVersion; - } - - sCachedFolder = parent; - sCachedFolderVersion = -1; - - for (String qualifier : Splitter.on('-').split(parent.getName())) { - Matcher matcher = VERSION_PATTERN.matcher(qualifier); - if (matcher.matches()) { - sCachedFolderVersion = Integer.parseInt(matcher.group(1)); - break; - } - } - - return sCachedFolderVersion; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/DefaultPosition.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/DefaultPosition.java deleted file mode 100644 index 72c8ee7..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/DefaultPosition.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.detector.api; - -import com.google.common.annotations.Beta; - -/** - * A simple offset-based position * - * <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 class DefaultPosition extends Position { - /** The line number (0-based where the first line is line 0) */ - private final int mLine; - - /** - * The column number (where the first character on the line is 0), or -1 if - * unknown - */ - private final int mColumn; - - /** The character offset */ - private final int mOffset; - - /** - * Creates a new {@link DefaultPosition} - * - * @param line the 0-based line number, or -1 if unknown - * @param column the 0-based column number, or -1 if unknown - * @param offset the offset, or -1 if unknown - */ - public DefaultPosition(int line, int column, int offset) { - mLine = line; - mColumn = column; - mOffset = offset; - } - - @Override - public int getLine() { - return mLine; - } - - @Override - public int getOffset() { - return mOffset; - } - - @Override - public int getColumn() { - return mColumn; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Detector.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Detector.java deleted file mode 100644 index a3412cf..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Detector.java +++ /dev/null @@ -1,611 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.LintDriver; -import com.google.common.annotations.Beta; -import lombok.ast.AstVisitor; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; - -/** - * A detector is able to find a particular problem. It might also be thought of as enforcing - * a rule, but "rule" is a bit overloaded in ADT terminology since ViewRules are used in - * the Rules API to allow views to specify designtime behavior in the graphical layout editor. - * <p> - * Each detector provides information about the issues it can find, such as an explanation - * of how to fix the issue, the priority, the category, etc. It also has an id which is - * used to persistently identify a particular type of error. - * <p> - * Detectors will be called in a predefined order: - * <ol> - * <li> Manifest file - * <li> Resource files, in alphabetical order by resource type - * (therefore, "layout" is checked before "values", "values-de" is checked before - * "values-en" but after "values", and so on. - * <li> Java sources - * <li> Java classes - * <li> Proguard files - * </ol> - * If a detector needs information when processing a file type that comes from a type of - * file later in the order above, they can request a second phase; see - * {@link LintDriver#requestRepeat}. - * <p> - * NOTE: Detectors might be constructed just once and shared between lint runs, so - * any per-detector state should be initialized and reset via the before/after - * methods. - * <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 Detector { - /** Specialized interface for detectors that scan Java source file parse trees */ - public interface JavaScanner { - /** - * Create a parse tree visitor to process the parse tree. All - * {@link JavaScanner} detectors must provide a visitor, unless they - * either return true from {@link #appliesToResourceRefs()} or return - * non null from {@link #getApplicableMethodNames()}. - * <p> - * If you return specific AST node types from - * {@link #getApplicableNodeTypes()}, then the visitor will <b>only</b> - * be called for the specific requested node types. This is more - * efficient, since it allows many detectors that apply to only a small - * part of the AST (such as method call nodes) to share iteration of the - * majority of the parse tree. - * <p> - * If you return null from {@link #getApplicableNodeTypes()}, then your - * visitor will be called from the top and all node types visited. - * <p> - * Note that a new visitor is created for each separate compilation - * unit, so you can store per file state in the visitor. - * - * @param context the {@link Context} for the file being analyzed - * @return a visitor, or null. - */ - @Nullable - AstVisitor createJavaVisitor(@NonNull JavaContext context); - - /** - * Return the types of AST nodes that the visitor returned from - * {@link #createJavaVisitor(JavaContext)} should visit. See the - * documentation for {@link #createJavaVisitor(JavaContext)} for details - * on how the shared visitor is used. - * <p> - * If you return null from this method, then the visitor will process - * the full tree instead. - * <p> - * Note that for the shared visitor, the return codes from the visit - * methods are ignored: returning true will <b>not</b> prune iteration - * of the subtree, since there may be other node types interested in the - * children. If you need to ensure that your visitor only processes a - * part of the tree, use a full visitor instead. See the - * OverdrawDetector implementation for an example of this. - * - * @return the list of applicable node types (AST node classes), or null - */ - @Nullable - List<Class<? extends Node>> getApplicableNodeTypes(); - - /** - * Return the list of method names this detector is interested in, or - * null. If this method returns non-null, then any AST nodes that match - * a method call in the list will be passed to the - * {@link #visitMethod(JavaContext, AstVisitor, MethodInvocation)} - * method for processing. The visitor created by - * {@link #createJavaVisitor(JavaContext)} is also passed to that - * method, although it can be null. - * <p> - * This makes it easy to write detectors that focus on some fixed calls. - * For example, the StringFormatDetector uses this mechanism to look for - * "format" calls, and when found it looks around (using the AST's - * {@link Node#getParent()} method) to see if it's called on - * a String class instance, and if so do its normal processing. Note - * that since it doesn't need to do any other AST processing, that - * detector does not actually supply a visitor. - * - * @return a set of applicable method names, or null. - */ - @Nullable - List<String> getApplicableMethodNames(); - - /** - * Method invoked for any method calls found that matches any names - * returned by {@link #getApplicableMethodNames()}. This also passes - * back the visitor that was created by - * {@link #createJavaVisitor(JavaContext)}, but a visitor is not - * required. It is intended for detectors that need to do additional AST - * processing, but also want the convenience of not having to look for - * method names on their own. - * - * @param context the context of the lint request - * @param visitor the visitor created from - * {@link #createJavaVisitor(JavaContext)}, or null - * @param node the {@link MethodInvocation} node for the invoked method - */ - void visitMethod( - @NonNull JavaContext context, - @Nullable AstVisitor visitor, - @NonNull MethodInvocation node); - - /** - * Returns whether this detector cares about Android resource references - * (such as {@code R.layout.main} or {@code R.string.app_name}). If it - * does, then the visitor will look for these patterns, and if found, it - * will invoke {@link #visitResourceReference} passing the resource type - * and resource name. It also passes the visitor, if any, that was - * created by {@link #createJavaVisitor(JavaContext)}, such that a - * detector can do more than just look for resources. - * - * @return true if this detector wants to be notified of R resource - * identifiers found in the code. - */ - boolean appliesToResourceRefs(); - - /** - * Called for any resource references (such as {@code R.layout.main} - * found in Java code, provided this detector returned {@code true} from - * {@link #appliesToResourceRefs()}. - * - * @param context the lint scanning context - * @param visitor the visitor created from - * {@link #createJavaVisitor(JavaContext)}, or null - * @param node the variable reference for the resource - * @param type the resource type, such as "layout" or "string" - * @param name the resource name, such as "main" from - * {@code R.layout.main} - * @param isFramework whether the resource is a framework resource - * (android.R) or a local project resource (R) - */ - void visitResourceReference( - @NonNull JavaContext context, - @Nullable AstVisitor visitor, - @NonNull Node node, - @NonNull String type, - @NonNull String name, - boolean isFramework); - } - - /** Specialized interface for detectors that scan Java class files */ - public interface ClassScanner { - /** - * Checks the given class' bytecode for issues. - * - * @param context the context of the lint check, pointing to for example - * the file - * @param classNode the root class node - */ - void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode); - - /** - * Returns the list of node types (corresponding to the constants in the - * {@link AbstractInsnNode} class) that this scanner applies to. The - * {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)} - * method will be called for each match. - * - * @return an array containing all the node types this detector should be - * called for, or null if none. - */ - @Nullable - int[] getApplicableAsmNodeTypes(); - - /** - * Process a given instruction node, and register lint issues if - * applicable. - * - * @param context the context of the lint check, pointing to for example - * the file - * @param classNode the root class node - * @param method the method node containing the call - * @param instruction the actual instruction - */ - void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull AbstractInsnNode instruction); - - /** - * Return the list of method call names (in VM format, e.g. "<init>" for - * constructors, etc) for method calls this detector is interested in, - * or null. T his will be used to dispatch calls to - * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} - * for only the method calls in owners that the detector is interested - * in. - * <p> - * <b>NOTE</b>: If you return non null from this method, then <b>only</b> - * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} - * will be called if a suitable method is found; - * {@link #checkClass(ClassContext, ClassNode)} will not be called under - * any circumstances. - * <p> - * This makes it easy to write detectors that focus on some fixed calls, - * and allows lint to make a single pass over the bytecode over a class, - * and efficiently dispatch method calls to any detectors that are - * interested in it. Without this, each new lint check interested in a - * single method, would be doing a complete pass through all the - * bytecode instructions of the class via the - * {@link #checkClass(ClassContext, ClassNode)} method, which would make - * each newly added lint check make lint slower. Now a single dispatch - * map is used instead, and for each encountered call in the single - * dispatch, it looks up in the map which if any detectors are - * interested in the given call name, and dispatches to each one in - * turn. - * - * @return a list of applicable method names, or null. - */ - @Nullable - List<String> getApplicableCallNames(); - - /** - * Just like {@link Detector#getApplicableCallNames()}, but for the owner - * field instead. The - * {@link #checkCall(ClassContext, ClassNode, MethodNode, MethodInsnNode)} - * method will be called for all {@link MethodInsnNode} instances where the - * owner field matches any of the members returned in this node. - * <p> - * Note that if your detector provides both a name and an owner, the - * method will be called for any nodes matching either the name <b>or</b> - * the owner, not only where they match <b>both</b>. Note also that it will - * be called twice - once for the name match, and (at least once) for the owner - * match. - * - * @return a list of applicable owner names, or null. - */ - @Nullable - List<String> getApplicableCallOwners(); - - /** - * Process a given method call node, and register lint issues if - * applicable. This is similar to the - * {@link #checkInstruction(ClassContext, ClassNode, MethodNode, AbstractInsnNode)} - * method, but has the additional advantage that it is only called for known - * method names or method owners, according to - * {@link #getApplicableCallNames()} and {@link #getApplicableCallOwners()}. - * - * @param context the context of the lint check, pointing to for example - * the file - * @param classNode the root class node - * @param method the method node containing the call - * @param call the actual method call node - */ - void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call); - } - - /** Specialized interface for detectors that scan XML files */ - public interface XmlScanner { - /** - * Visit the given document. The detector is responsible for its own iteration - * through the document. - * @param context information about the document being analyzed - * @param document the document to examine - */ - void visitDocument(@NonNull XmlContext context, @NonNull Document document); - - /** - * Visit the given element. - * @param context information about the document being analyzed - * @param element the element to examine - */ - void visitElement(@NonNull XmlContext context, @NonNull Element element); - - /** - * Visit the given element after its children have been analyzed. - * @param context information about the document being analyzed - * @param element the element to examine - */ - void visitElementAfter(@NonNull XmlContext context, @NonNull Element element); - - /** - * Visit the given attribute. - * @param context information about the document being analyzed - * @param attribute the attribute node to examine - */ - void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute); - - /** - * Returns the list of elements that this detector wants to analyze. If non - * null, this detector will be called (specifically, the - * {@link #visitElement} method) for each matching element in the document. - * <p> - * If this method returns null, and {@link #getApplicableAttributes()} also returns - * null, then the {@link #visitDocument} method will be called instead. - * - * @return a collection of elements, or null, or the special - * {@link XmlScanner#ALL} marker to indicate that every single - * element should be analyzed. - */ - @Nullable - Collection<String> getApplicableElements(); - - /** - * Returns the list of attributes that this detector wants to analyze. If non - * null, this detector will be called (specifically, the - * {@link #visitAttribute} method) for each matching attribute in the document. - * <p> - * If this method returns null, and {@link #getApplicableElements()} also returns - * null, then the {@link #visitDocument} method will be called instead. - * - * @return a collection of attributes, or null, or the special - * {@link XmlScanner#ALL} marker to indicate that every single - * attribute should be analyzed. - */ - @Nullable - Collection<String> getApplicableAttributes(); - - /** - * Special marker collection returned by {@link #getApplicableElements()} or - * {@link #getApplicableAttributes()} to indicate that the check should be - * invoked on all elements or all attributes - */ - @NonNull - List<String> ALL = new ArrayList<String>(0); // NOT Collections.EMPTY! - // We want to distinguish this from just an *empty* list returned by the caller! - } - - /** Specialized interface for detectors that scan other files */ - public interface OtherFileScanner { - /** - * Returns the set of files this scanner wants to consider. If this includes - * {@link Scope#OTHER} then all source files will be checked. Note that the - * set of files will not just include files of the indicated type, but all files - * within the relevant source folder. For example, returning {@link Scope#JAVA_FILE} - * will not just return {@code .java} files, but also other resource files such as - * {@code .html} and other files found within the Java source folders. - * <p> - * Lint will call the {@link #run(Context)}} method when the file should be checked. - * - * @return set of scopes that define the types of source files the - * detector wants to consider - */ - @NonNull - EnumSet<Scope> getApplicableFiles(); - } - - /** - * Runs the detector. This method will not be called for certain specialized - * detectors, such as {@link XmlScanner} and {@link JavaScanner}, where - * there are specialized analysis methods instead such as - * {@link XmlScanner#visitElement(XmlContext, Element)}. - * - * @param context the context describing the work to be done - */ - public void run(@NonNull Context context) { - } - - /** - * Returns true if this detector applies to the given file - * - * @param context the context to check - * @param file the file in the context to check - * @return true if this detector applies to the given context and file - */ - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return false; - } - - /** - * Analysis is about to begin, perform any setup steps. - * - * @param context the context for the check referencing the project, lint - * client, etc - */ - public void beforeCheckProject(@NonNull Context context) { - } - - /** - * Analysis has just been finished for the whole project, perform any - * cleanup or report issues that require project-wide analysis. - * - * @param context the context for the check referencing the project, lint - * client, etc - */ - public void afterCheckProject(@NonNull Context context) { - } - - /** - * Analysis is about to begin for the given library project, perform any setup steps. - * - * @param context the context for the check referencing the project, lint - * client, etc - */ - public void beforeCheckLibraryProject(@NonNull Context context) { - } - - /** - * Analysis has just been finished for the given library project, perform any - * cleanup or report issues that require library-project-wide analysis. - * - * @param context the context for the check referencing the project, lint - * client, etc - */ - public void afterCheckLibraryProject(@NonNull Context context) { - } - - /** - * Analysis is about to be performed on a specific file, perform any setup - * steps. - * <p> - * Note: When this method is called at the beginning of checking an XML - * file, the context is guaranteed to be an instance of {@link XmlContext}, - * and similarly for a Java source file, the context will be a - * {@link JavaContext} and so on. - * - * @param context the context for the check referencing the file to be - * checked, the project, etc. - */ - public void beforeCheckFile(@NonNull Context context) { - } - - /** - * Analysis has just been finished for a specific file, perform any cleanup - * or report issues found - * <p> - * Note: When this method is called at the end of checking an XML - * file, the context is guaranteed to be an instance of {@link XmlContext}, - * and similarly for a Java source file, the context will be a - * {@link JavaContext} and so on. - * - * @param context the context for the check referencing the file to be - * checked, the project, etc. - */ - public void afterCheckFile(@NonNull Context context) { - } - - /** - * Returns the expected speed of this detector - * - * @return the expected speed of this detector - */ - @NonNull - public Speed getSpeed() { - return Speed.NORMAL; - } - - // ---- Dummy implementations to make implementing XmlScanner easier: ---- - - @SuppressWarnings("javadoc") - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - // This method must be overridden if your detector does - // not return something from getApplicableElements or - // getApplicableAttributes - assert false; - } - - @SuppressWarnings("javadoc") - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - // This method must be overridden if your detector returns - // tag names from getApplicableElements - assert false; - } - - @SuppressWarnings("javadoc") - public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) { - } - - @SuppressWarnings("javadoc") - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - // This method must be overridden if your detector returns - // attribute names from getApplicableAttributes - assert false; - } - - @SuppressWarnings("javadoc") - @Nullable - public Collection<String> getApplicableElements() { - return null; - } - - @Nullable - @SuppressWarnings("javadoc") - public Collection<String> getApplicableAttributes() { - return null; - } - - // ---- Dummy implementations to make implementing JavaScanner easier: ---- - - @Nullable @SuppressWarnings("javadoc") - public List<String> getApplicableMethodNames() { - return null; - } - - @Nullable @SuppressWarnings("javadoc") - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return null; - } - - @Nullable @SuppressWarnings("javadoc") - public List<Class<? extends Node>> getApplicableNodeTypes() { - return null; - } - - @SuppressWarnings("javadoc") - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - } - - @SuppressWarnings("javadoc") - public boolean appliesToResourceRefs() { - return false; - } - - @SuppressWarnings("javadoc") - public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull Node node, @NonNull String type, @NonNull String name, - boolean isFramework) { - } - - // ---- Dummy implementations to make implementing a ClassScanner easier: ---- - - @SuppressWarnings("javadoc") - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - } - - @SuppressWarnings("javadoc") - @Nullable - public List<String> getApplicableCallNames() { - return null; - } - - @SuppressWarnings("javadoc") - @Nullable - public List<String> getApplicableCallOwners() { - return null; - } - - @SuppressWarnings("javadoc") - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - } - - @SuppressWarnings("javadoc") - @Nullable - public int[] getApplicableAsmNodeTypes() { - return null; - } - - @SuppressWarnings("javadoc") - public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull AbstractInsnNode instruction) { - } - - // ---- Dummy implementations to make implementing an OtherFileScanner easier: ---- - - public boolean appliesToFolder(@NonNull Scope scope, @Nullable ResourceFolderType folderType) { - return false; - } - - @NonNull - public EnumSet<Scope> getApplicableFiles() { - return Scope.OTHER_SCOPE; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Issue.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Issue.java deleted file mode 100644 index ceca9ca..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Issue.java +++ /dev/null @@ -1,553 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.Configuration; -import com.android.tools.lint.client.api.IssueRegistry; -import com.google.common.annotations.Beta; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumSet; -import java.util.List; - - -/** - * An issue is a potential bug in an Android application. An issue is discovered - * by a {@link Detector}, and has an associated {@link Severity}. - * <p> - * Issues and detectors are separate classes because a detector can discover - * multiple different issues as it's analyzing code, and we want to be able to - * different severities for different issues, the ability to suppress one but - * not other issues from the same detector, and so on. - * <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 final class Issue implements Comparable<Issue> { - private static final String HTTP_PREFIX = "http://"; //$NON-NLS-1$ - - private final String mId; - private final String mDescription; - private final String mExplanation; - private final Category mCategory; - private final int mPriority; - private final Severity mSeverity; - private String mMoreInfoUrl; - private boolean mEnabledByDefault = true; - private final EnumSet<Scope> mScope; - private List<EnumSet<Scope>> mAnalysisScopes; - private final Class<? extends Detector> mClass; - - // Use factory methods - private Issue( - @NonNull String id, - @NonNull String description, - @NonNull String explanation, - @NonNull Category category, - int priority, - @NonNull Severity severity, - @NonNull Class<? extends Detector> detectorClass, - @NonNull EnumSet<Scope> scope) { - super(); - mId = id; - mDescription = description; - mExplanation = explanation; - mCategory = category; - mPriority = priority; - mSeverity = severity; - mClass = detectorClass; - mScope = scope; - } - - /** - * Creates a new issue - * - * @param id the fixed id of the issue - * @param description the quick summary of the issue (one line) - * @param explanation a full explanation of the issue, with suggestions for - * how to fix it - * @param category the associated category, if any - * @param priority the priority, a number from 1 to 10 with 10 being most - * important/severe - * @param severity the default severity of the issue - * @param detectorClass the class of the detector to find this issue - * @param scope the scope of files required to analyze this issue - * @return a new {@link Issue} - */ - @NonNull - public static Issue create( - @NonNull String id, - @NonNull String description, - @NonNull String explanation, - @NonNull Category category, - int priority, - @NonNull Severity severity, - @NonNull Class<? extends Detector> detectorClass, - @NonNull EnumSet<Scope> scope) { - return new Issue(id, description, explanation, category, priority, severity, - detectorClass, scope); - } - - /** - * Returns the unique id of this issue. These should not change over time - * since they are used to persist the names of issues suppressed by the user - * etc. It is typically a single camel-cased word. - * - * @return the associated fixed id, never null and always unique - */ - @NonNull - public String getId() { - return mId; - } - - /** - * Briefly (one line) describes the kinds of checks performed by this rule - * - * @return a quick summary of the issue, never null - */ - @NonNull - public String getDescription() { - return mDescription; - } - - /** - * Describes the error found by this rule, e.g. - * "Buttons must define contentDescriptions". Preferably the explanation - * should also contain a description of how the problem should be solved. - * Additional info can be provided via {@link #getMoreInfo()}. - * <p> - * Note that the text may contain some simple markup, such as *'s around sentences - * for bold text, and back quotes (`) for code fragments. You can obtain - * the text without this markup by calling {@link #getExplanationAsSimpleText()}, - * and you can obtain the text as annotated HTML by calling - * {@link #getExplanationAsHtml()}. - * - * @return an explanation of the issue, never null. - */ - @NonNull - public String getExplanation() { - return mExplanation; - } - - /** - * Like {@link #getExplanation()}, but returns the text as properly escaped - * and marked up HTML, where http URLs are linked, where words with asterisks - * such as *this* are shown in bold, etc. - * - * @return the explanation of the issue, never null - */ - @NonNull - public String getExplanationAsHtml() { - return convertMarkup(mExplanation, true /* html */); - } - - /** - * Like {@link #getExplanation()}, but returns the text as properly escaped - * and marked up HTML, where http URLs are linked, where words with asterisks - * such as *this* are shown in bold, etc. - * - * @return the explanation of the issue, never null - */ - @NonNull - public String getExplanationAsSimpleText() { - return convertMarkup(mExplanation, false /* not html = text */); - } - - /** - * The primary category of the issue - * - * @return the primary category of the issue, never null - */ - @NonNull - public Category getCategory() { - return mCategory; - } - - /** - * Returns a priority, in the range 1-10, with 10 being the most severe and - * 1 the least - * - * @return a priority from 1 to 10 - */ - public int getPriority() { - return mPriority; - } - - /** - * Returns the default severity of the issues found by this detector (some - * tools may allow the user to specify custom severities for detectors). - * <p> - * Note that even though the normal way for an issue to be disabled is for - * the {@link Configuration} to return {@link Severity#IGNORE}, there is a - * {@link #isEnabledByDefault()} method which can be used to turn off issues - * by default. This is done rather than just having the severity as the only - * attribute on the issue such that an issue can be configured with an - * appropriate severity (such as {@link Severity#ERROR}) even when issues - * are disabled by default for example because they are experimental or not - * yet stable. - * - * @return the severity of the issues found by this detector - */ - @NonNull - public Severity getDefaultSeverity() { - return mSeverity; - } - - /** - * Returns a link (a URL string) to more information, or null - * - * @return a link to more information, or null - */ - @Nullable - public String getMoreInfo() { - return mMoreInfoUrl; - } - - /** - * Returns whether this issue should be enabled by default, unless the user - * has explicitly disabled it. - * - * @return true if this issue should be enabled by default - */ - public boolean isEnabledByDefault() { - return mEnabledByDefault; - } - - /** - * Returns the scope required to analyze the code to detect this issue. - * This is determined by the detectors which reports the issue. - * - * @return the required scope - */ - @NonNull - public EnumSet<Scope> getScope() { - return mScope; - } - - /** - * Sorts the detectors alphabetically by id. This is intended to make it - * convenient to store settings for detectors in a fixed order. It is not - * intended as the order to be shown to the user; for that, a tool embedding - * lint might consider the priorities, categories, severities etc of the - * various detectors. - * - * @param other the {@link Issue} to compare this issue to - */ - @Override - public int compareTo(Issue other) { - return getId().compareTo(other.getId()); - } - - /** - * Sets a more info URL string - * - * @param moreInfoUrl url string - * @return this, for constructor chaining - */ - @NonNull - public Issue setMoreInfo(@NonNull String moreInfoUrl) { - mMoreInfoUrl = moreInfoUrl; - return this; - } - - /** - * Sets whether this issue is enabled by default. - * - * @param enabledByDefault whether the issue should be enabled by default - * @return this, for constructor chaining - */ - @NonNull - public Issue setEnabledByDefault(boolean enabledByDefault) { - mEnabledByDefault = enabledByDefault; - return this; - } - - /** - * Returns the sets of scopes required to analyze this issue, or null if all - * scopes named by {@link Issue#getScope()} are necessary. Note that only - * <b>one</b> match out of this collection is required, not all, and that - * the scope set returned by {@link #getScope()} does not have to be returned - * by this method, but is always implied to be included. - * <p> - * The scopes returned by {@link Issue#getScope()} list all the various - * scopes that are <b>affected</b> by this issue, meaning the detector - * should consider it. Frequently, the detector must analyze all these - * scopes in order to properly decide whether an issue is found. For - * example, the unused resource detector needs to consider both the XML - * resource files and the Java source files in order to decide if a resource - * is unused. If it analyzes just the Java files for example, it might - * incorrectly conclude that a resource is unused because it did not - * discover a resource reference in an XML file. - * <p> - * However, there are other issues where the issue can occur in a variety of - * files, but the detector can consider each in isolation. For example, the - * API checker is affected by both XML files and Java class files (detecting - * both layout constructor references in XML layout files as well as code - * references in .class files). It doesn't have to analyze both; it is - * capable of incrementally analyzing just an XML file, or just a class - * file, without considering the other. - * <p> - * The required scope list provides a list of scope sets that can be used to - * analyze this issue. For each scope set, all the scopes must be matched by - * the incremental analysis, but any one of the scope sets can be analyzed - * in isolation. - * <p> - * The required scope list is not required to include the full scope set - * returned by {@link #getScope()}; that set is always assumed to be - * included. - * <p> - * NOTE: You would normally call {@link #isAdequate(EnumSet)} rather - * than calling this method directly. - * - * @return a list of required scopes, or null. - */ - @Nullable - public Collection<EnumSet<Scope>> getAnalysisScopes() { - return mAnalysisScopes; - } - - /** - * Sets the collection of scopes that are allowed to be analyzed independently. - * See the {@link #getAnalysisScopes()} method for a full explanation. - * Note that you usually want to just call {@link #addAnalysisScope(EnumSet)} - * instead of constructing a list up front and passing it in here. This - * method exists primarily such that commonly used share sets of analysis - * scopes can be reused and set directly. - * - * @param required the collection of scopes - * @return this, for constructor chaining - */ - @NonNull - public Issue setAnalysisScopes(@Nullable List<EnumSet<Scope>> required) { - mAnalysisScopes = required; - - return this; - } - - /** - * Returns true if the given scope is adequate for analyzing this issue. - * This looks through the analysis scopes (see - * {@link #addAnalysisScope(EnumSet)}) and if the scope passed in fully - * covers at least one of them, or if it covers the scope of the issue - * itself (see {@link #getScope()}, which should be a superset of all the - * analysis scopes) returns true. - * <p> - * The scope set returned by {@link Issue#getScope()} lists all the various - * scopes that are <b>affected</b> by this issue, meaning the detector - * should consider it. Frequently, the detector must analyze all these - * scopes in order to properly decide whether an issue is found. For - * example, the unused resource detector needs to consider both the XML - * resource files and the Java source files in order to decide if a resource - * is unused. If it analyzes just the Java files for example, it might - * incorrectly conclude that a resource is unused because it did not - * discover a resource reference in an XML file. - * <p> - * However, there are other issues where the issue can occur in a variety of - * files, but the detector can consider each in isolation. For example, the - * API checker is affected by both XML files and Java class files (detecting - * both layout constructor references in XML layout files as well as code - * references in .class files). It doesn't have to analyze both; it is - * capable of incrementally analyzing just an XML file, or just a class - * file, without considering the other. - * <p> - * An issue can register additional scope sets that can are adequate - * for analyzing the issue, by calling {@link #addAnalysisScope(EnumSet)}. - * This method returns true if the given scope matches one or more analysis - * scope, or the overall scope. - * - * @param scope the scope available for analysis - * @return true if this issue can be analyzed with the given available scope - */ - public boolean isAdequate(@NonNull EnumSet<Scope> scope) { - if (scope.containsAll(mScope)) { - return true; - } - - if (mAnalysisScopes != null) { - for (EnumSet<Scope> analysisScope : mAnalysisScopes) { - if (mScope.containsAll(analysisScope)) { - return true; - } - } - } - - if (this == IssueRegistry.LINT_ERROR || this == IssueRegistry.PARSER_ERROR) { - return true; - } - - return false; - } - - /** - * Adds a scope set that can be analyzed independently to uncover this issue. - * See the {@link #getAnalysisScopes()} method for a full explanation. - * Note that the {@link #getScope()} does not have to be added here; it is - * always considered an analysis scope. - * - * @param scope the additional scope which can analyze this issue independently - * @return this, for constructor chaining - */ - public Issue addAnalysisScope(@Nullable EnumSet<Scope> scope) { - if (mAnalysisScopes == null) { - mAnalysisScopes = new ArrayList<EnumSet<Scope>>(2); - } - mAnalysisScopes.add(scope); - - return this; - } - - /** - * Returns the class of the detector to use to find this issue - * - * @return the class of the detector to use to find this issue - */ - @NonNull - public Class<? extends Detector> getDetectorClass() { - return mClass; - } - - @Override - public String toString() { - return mId; - } - - /** - * Converts the given markup text to HTML or text, depending on the. - * <p> - * This will recognize the following formatting conventions: - * <ul> - * <li>HTTP urls (http://...) - * <li>Sentences immediately surrounded by * will be shown as bold. - * <li>Sentences immediately surrounded by ` will be shown using monospace - * fonts - * </ul> - * Furthermore, newlines are converted to br's when converting newlines. - * Note: It does not insert {@code <html>} tags around the fragment for HTML output. - * <p> - * TODO: Consider switching to the restructured text format - - * http://docutils.sourceforge.net/docs/user/rst/quickstart.html - * - * @param text the text to be formatted - * @param html whether to convert into HTML or text - * @return the corresponding HTML or text properly formatted - */ - @NonNull - public static String convertMarkup(@NonNull String text, boolean html) { - StringBuilder sb = new StringBuilder(3 * text.length() / 2); - - char prev = 0; - int flushIndex = 0; - int n = text.length(); - for (int i = 0; i < n; i++) { - char c = text.charAt(i); - if ((c == '*' || c == '`' && i < n - 1)) { - // Scout ahead for range end - if (!Character.isLetterOrDigit(prev) - && !Character.isWhitespace(text.charAt(i + 1))) { - // Found * or ~ immediately before a letter, and not in the middle of a word - // Find end - int end = text.indexOf(c, i + 1); - if (end != -1 && (end == n - 1 || !Character.isLetter(text.charAt(end + 1)))) { - if (i > flushIndex) { - appendEscapedText(sb, text, html, flushIndex, i); - } - if (html) { - String tag = c == '*' ? "b" : "code"; //$NON-NLS-1$ //$NON-NLS-2$ - sb.append('<').append(tag).append('>'); - appendEscapedText(sb, text, html, i + 1, end); - sb.append('<').append('/').append(tag).append('>'); - } else { - appendEscapedText(sb, text, html, i + 1, end); - } - flushIndex = end + 1; - i = flushIndex - 1; // -1: account for the i++ in the loop - } - } - } else if (html && c == 'h' && i < n - 1 && text.charAt(i + 1) == 't' - && text.startsWith(HTTP_PREFIX, i) && !Character.isLetterOrDigit(prev)) { - // Find url end - int end = i + HTTP_PREFIX.length(); - while (end < n) { - char d = text.charAt(end); - if (Character.isWhitespace(d)) { - break; - } - end++; - } - char last = text.charAt(end - 1); - if (last == '.' || last == ')' || last == '!') { - end--; - } - if (end > i + HTTP_PREFIX.length()) { - if (i > flushIndex) { - appendEscapedText(sb, text, html, flushIndex, i); - } - - String url = text.substring(i, end); - sb.append("<a href=\""); //$NON-NLS-1$ - sb.append(url); - sb.append('"').append('>'); - sb.append(url); - sb.append("</a>"); //$NON-NLS-1$ - - flushIndex = end; - i = flushIndex - 1; // -1: account for the i++ in the loop - } - } - prev = c; - } - - if (flushIndex < n) { - appendEscapedText(sb, text, html, flushIndex, n); - } - - return sb.toString(); - } - - private static void appendEscapedText(StringBuilder sb, String text, boolean html, - int start, int end) { - if (html) { - for (int i = start; i < end; i++) { - char c = text.charAt(i); - if (c == '<') { - sb.append("<"); //$NON-NLS-1$ - } else if (c == '&') { - sb.append("&"); //$NON-NLS-1$ - } else if (c == '\n') { - sb.append("<br/>\n"); - } else { - if (c > 255) { - sb.append("&#"); //$NON-NLS-1$ - sb.append(Integer.toString(c)); - sb.append(';'); - } else { - sb.append(c); - } - } - } - } else { - for (int i = start; i < end; i++) { - char c = text.charAt(i); - sb.append(c); - } - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java deleted file mode 100644 index d91f423..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/JavaContext.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.IJavaParser; -import com.android.tools.lint.client.api.LintDriver; - -import java.io.File; - -import lombok.ast.ConstructorDeclaration; -import lombok.ast.MethodDeclaration; -import lombok.ast.Node; - -/** - * A {@link Context} used when checking Java files. - * <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> - */ -public class JavaContext extends Context { - /** The parse tree */ - public Node compilationUnit; - /** The parser which produced the parse tree */ - public IJavaParser parser; - - /** - * Constructs a {@link JavaContext} for running lint on the given file, with - * the given scope, in the given project reporting errors to the given - * client. - * - * @param driver the driver running through the checks - * @param project the project to run lint on which contains the given file - * @param main the main project if this project is a library project, or - * null if this is not a library project. The main project is - * the root project of all library projects, not necessarily the - * directly including project. - * @param file the file to be analyzed - */ - public JavaContext( - @NonNull LintDriver driver, - @NonNull Project project, - @Nullable Project main, - @NonNull File file) { - super(driver, project, main, file); - } - - /** - * Returns a location for the given node - * - * @param node the AST node to get a location for - * @return a location for the given node - */ - @NonNull - public Location getLocation(@NonNull Node node) { - if (parser != null) { - return parser.getLocation(this, node); - } - - return new Location(file, null, null); - } - - @Override - public void report(@NonNull Issue issue, @Nullable Location location, - @NonNull String message, @Nullable Object data) { - if (mDriver.isSuppressed(issue, compilationUnit)) { - return; - } - super.report(issue, location, message, data); - } - - /** - * Reports an issue applicable to a given AST node. The AST node is used as the - * scope to check for suppress lint annotations. - * - * @param issue the issue to report - * @param scope the AST node scope the error applies to. The lint infrastructure - * will check whether there are suppress annotations on this node (or its enclosing - * nodes) and if so suppress the warning without involving the client. - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - public void report( - @NonNull Issue issue, - @Nullable Node scope, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - if (scope != null && mDriver.isSuppressed(issue, scope)) { - return; - } - super.report(issue, location, message, data); - } - - - @Nullable - public static Node findSurroundingMethod(Node scope) { - while (scope != null) { - Class<? extends Node> type = scope.getClass(); - // The Lombok AST uses a flat hierarchy of node type implementation classes - // so no need to do instanceof stuff here. - if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) { - return scope; - } - - scope = scope.getParent(); - } - - return null; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LayoutDetector.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LayoutDetector.java deleted file mode 100644 index b24c1a9..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LayoutDetector.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.detector.api; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.ATTR_PADDING; -import static com.android.SdkConstants.ATTR_PADDING_BOTTOM; -import static com.android.SdkConstants.ATTR_PADDING_LEFT; -import static com.android.SdkConstants.ATTR_PADDING_RIGHT; -import static com.android.SdkConstants.ATTR_PADDING_TOP; -import static com.android.SdkConstants.VALUE_FILL_PARENT; -import static com.android.SdkConstants.VALUE_MATCH_PARENT; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.google.common.annotations.Beta; - -import org.w3c.dom.Element; - -/** - * Abstract class specifically intended for layout detectors which provides some - * common utility methods shared by layout detectors. - * <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 LayoutDetector extends ResourceXmlDetector { - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT; - } - - private static boolean isFillParent(@NonNull Element element, @NonNull String dimension) { - String width = element.getAttributeNS(ANDROID_URI, dimension); - return width.equals(VALUE_MATCH_PARENT) || width.equals(VALUE_FILL_PARENT); - } - - protected static boolean isWidthFillParent(@NonNull Element element) { - return isFillParent(element, ATTR_LAYOUT_WIDTH); - } - - protected static boolean isHeightFillParent(@NonNull Element element) { - return isFillParent(element, ATTR_LAYOUT_HEIGHT); - } - - protected boolean hasPadding(@NonNull Element root) { - return root.hasAttributeNS(ANDROID_URI, ATTR_PADDING) - || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_LEFT) - || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_RIGHT) - || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_TOP) - || root.hasAttributeNS(ANDROID_URI, ATTR_PADDING_BOTTOM); - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LintUtils.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LintUtils.java deleted file mode 100644 index 9dec569..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/LintUtils.java +++ /dev/null @@ -1,798 +0,0 @@ -/* - * 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.detector.api; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.BIN_FOLDER; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.ID_PREFIX; -import static com.android.SdkConstants.NEW_ID_PREFIX; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.FolderTypeRelationship; -import com.android.resources.ResourceFolderType; -import com.android.resources.ResourceType; -import com.android.tools.lint.client.api.LintClient; -import com.android.utils.PositionXmlParser; -import com.google.common.annotations.Beta; -import com.google.common.base.Splitter; -import com.google.common.collect.Iterables; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldNode; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.List; - -import lombok.ast.ImportDeclaration; - - -/** - * Useful utility methods related to lint. - * <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 class LintUtils { - // Utility class, do not instantiate - private LintUtils() { - } - - /** - * Format a list of strings, and cut of the list at {@code maxItems} if the - * number of items are greater. - * - * @param strings the list of strings to print out as a comma separated list - * @param maxItems the maximum number of items to print - * @return a comma separated list - */ - @NonNull - public static String formatList(@NonNull List<String> strings, int maxItems) { - StringBuilder sb = new StringBuilder(20 * strings.size()); - - for (int i = 0, n = strings.size(); i < n; i++) { - if (sb.length() > 0) { - sb.append(", "); //$NON-NLS-1$ - } - sb.append(strings.get(i)); - - if (maxItems > 0 && i == maxItems - 1 && n > maxItems) { - sb.append(String.format("... (%1$d more)", n - i - 1)); - break; - } - } - - return sb.toString(); - } - - /** - * Determine if the given type corresponds to a resource that has a unique - * file - * - * @param type the resource type to check - * @return true if the given type corresponds to a file-type resource - */ - public static boolean isFileBasedResourceType(@NonNull ResourceType type) { - List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); - for (ResourceFolderType folderType : folderTypes) { - if (folderType != ResourceFolderType.VALUES) { - if (type == ResourceType.ID) { - return false; - } - return true; - } - } - return false; - } - - /** - * Returns true if the given file represents an XML file - * - * @param file the file to be checked - * @return true if the given file is an xml file - */ - public static boolean isXmlFile(@NonNull File file) { - String string = file.getName(); - return string.regionMatches(true, string.length() - DOT_XML.length(), - DOT_XML, 0, DOT_XML.length()); - } - - /** - * Case insensitive ends with - * - * @param string the string to be tested whether it ends with the given - * suffix - * @param suffix the suffix to check - * @return true if {@code string} ends with {@code suffix}, - * case-insensitively. - */ - public static boolean endsWith(@NonNull String string, @NonNull String suffix) { - return string.regionMatches(true /* ignoreCase */, string.length() - suffix.length(), - suffix, 0, suffix.length()); - } - - /** - * Case insensitive starts with - * - * @param string the string to be tested whether it starts with the given prefix - * @param prefix the prefix to check - * @param offset the offset to start checking with - * @return true if {@code string} starts with {@code prefix}, - * case-insensitively. - */ - public static boolean startsWith(@NonNull String string, @NonNull String prefix, int offset) { - return string.regionMatches(true /* ignoreCase */, offset, prefix, 0, prefix.length()); - } - - /** - * Returns the basename of the given filename, unless it's a dot-file such as ".svn". - * - * @param fileName the file name to extract the basename from - * @return the basename (the filename without the file extension) - */ - public static String getBaseName(@NonNull String fileName) { - int extension = fileName.indexOf('.'); - if (extension > 0) { - return fileName.substring(0, extension); - } else { - return fileName; - } - } - - /** - * Returns the children elements of the given node - * - * @param node the parent node - * @return a list of element children, never null - */ - @NonNull - public static List<Element> getChildren(@NonNull Node node) { - NodeList childNodes = node.getChildNodes(); - List<Element> children = new ArrayList<Element>(childNodes.getLength()); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - children.add((Element) child); - } - } - - return children; - } - - /** - * Returns the <b>number</b> of children of the given node - * - * @param node the parent node - * @return the count of element children - */ - public static int getChildCount(@NonNull Node node) { - NodeList childNodes = node.getChildNodes(); - int childCount = 0; - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE) { - childCount++; - } - } - - return childCount; - } - - /** - * Returns true if the given element is the root element of its document - * - * @param element the element to test - * @return true if the element is the root element - */ - public static boolean isRootElement(Element element) { - return element == element.getOwnerDocument().getDocumentElement(); - } - - /** - * Returns the given id without an {@code @id/} or {@code @+id} prefix - * - * @param id the id to strip - * @return the stripped id, never null - */ - @NonNull - public static String stripIdPrefix(@Nullable String id) { - if (id == null) { - return ""; - } else if (id.startsWith(NEW_ID_PREFIX)) { - return id.substring(NEW_ID_PREFIX.length()); - } else if (id.startsWith(ID_PREFIX)) { - return id.substring(ID_PREFIX.length()); - } - - return id; - } - - /** - * Returns true if the given two id references match. This is similar to - * String equality, but it also considers "{@code @+id/foo == @id/foo}. - * - * @param id1 the first id to compare - * @param id2 the second id to compare - * @return true if the two id references refer to the same id - */ - public static boolean idReferencesMatch(String id1, String id2) { - if (id1.startsWith(NEW_ID_PREFIX)) { - if (id2.startsWith(NEW_ID_PREFIX)) { - return id1.equals(id2); - } else { - assert id2.startsWith(ID_PREFIX); - return ((id1.length() - id2.length()) - == (NEW_ID_PREFIX.length() - ID_PREFIX.length())) - && id1.regionMatches(NEW_ID_PREFIX.length(), id2, - ID_PREFIX.length(), - id2.length() - ID_PREFIX.length()); - } - } else { - assert id1.startsWith(ID_PREFIX); - if (id2.startsWith(ID_PREFIX)) { - return id1.equals(id2); - } else { - assert id2.startsWith(NEW_ID_PREFIX); - return (id2.length() - id1.length() - == (NEW_ID_PREFIX.length() - ID_PREFIX.length())) - && id2.regionMatches(NEW_ID_PREFIX.length(), id1, - ID_PREFIX.length(), - id1.length() - ID_PREFIX.length()); - } - } - } - - /** - * Computes the edit distance (number of insertions, deletions or substitutions - * to edit one string into the other) between two strings. In particular, - * this will compute the Levenshtein distance. - * <p> - * See http://en.wikipedia.org/wiki/Levenshtein_distance for details. - * - * @param s the first string to compare - * @param t the second string to compare - * @return the edit distance between the two strings - */ - public static int editDistance(@NonNull String s, @NonNull String t) { - int m = s.length(); - int n = t.length(); - int[][] d = new int[m + 1][n + 1]; - for (int i = 0; i <= m; i++) { - d[i][0] = i; - } - for (int j = 0; j <= n; j++) { - d[0][j] = j; - } - for (int j = 1; j <= n; j++) { - for (int i = 1; i <= m; i++) { - if (s.charAt(i - 1) == t.charAt(j - 1)) { - d[i][j] = d[i - 1][j - 1]; - } else { - int deletion = d[i - 1][j] + 1; - int insertion = d[i][j - 1] + 1; - int substitution = d[i - 1][j - 1] + 1; - d[i][j] = Math.min(deletion, Math.min(insertion, substitution)); - } - } - } - - return d[m][n]; - } - - /** - * Returns true if assertions are enabled - * - * @return true if assertions are enabled - */ - @SuppressWarnings("all") - public static boolean assertionsEnabled() { - boolean assertionsEnabled = false; - assert assertionsEnabled = true; // Intentional side-effect - return assertionsEnabled; - } - - /** - * Returns the layout resource name for the given layout file - * - * @param layoutFile the file pointing to the layout - * @return the layout resource name, not including the {@code @layout} - * prefix - */ - public static String getLayoutName(File layoutFile) { - String name = layoutFile.getName(); - int dotIndex = name.indexOf('.'); - if (dotIndex != -1) { - name = name.substring(0, dotIndex); - } - return name; - } - - /** - * Splits the given path into its individual parts, attempting to be - * tolerant about path separators (: or ;). It can handle possibly ambiguous - * paths, such as {@code c:\foo\bar:\other}, though of course these are to - * be avoided if possible. - * - * @param path the path variable to split, which can use both : and ; as - * path separators. - * @return the individual path components as an Iterable of strings - */ - public static Iterable<String> splitPath(String path) { - if (path.indexOf(';') != -1) { - return Splitter.on(';').omitEmptyStrings().trimResults().split(path); - } - - List<String> combined = new ArrayList<String>(); - Iterables.addAll(combined, Splitter.on(':').omitEmptyStrings().trimResults().split(path)); - for (int i = 0, n = combined.size(); i < n; i++) { - String p = combined.get(i); - if (p.length() == 1 && i < n - 1 && Character.isLetter(p.charAt(0)) - // Technically, Windows paths do not have to have a \ after the :, - // which means it would be using the current directory on that drive, - // but that's unlikely to be the case in a path since it would have - // unpredictable results - && !combined.get(i+1).isEmpty() && combined.get(i+1).charAt(0) == '\\') { - combined.set(i, p + ':' + combined.get(i+1)); - combined.remove(i+1); - n--; - continue; - } - } - - return combined; - } - - /** - * Computes the shared parent among a set of files (which may be null). - * - * @param files the set of files to be checked - * @return the closest common ancestor file, or null if none was found - */ - @Nullable - public static File getCommonParent(@NonNull List<File> files) { - int fileCount = files.size(); - if (fileCount == 0) { - return null; - } else if (fileCount == 1) { - return files.get(0); - } else if (fileCount == 2) { - return getCommonParent(files.get(0), files.get(1)); - } else { - File common = files.get(0); - for (int i = 1; i < fileCount; i++) { - common = getCommonParent(common, files.get(i)); - if (common == null) { - return null; - } - } - - return common; - } - } - - /** - * Computes the closest common parent path between two files. - * - * @param file1 the first file to be compared - * @param file2 the second file to be compared - * @return the closest common ancestor file, or null if the two files have - * no common parent - */ - @Nullable - public static File getCommonParent(@NonNull File file1, @NonNull File file2) { - if (file1.equals(file2)) { - return file1; - } else if (file1.getPath().startsWith(file2.getPath())) { - return file2; - } else if (file2.getPath().startsWith(file1.getPath())) { - return file1; - } else { - // Dumb and simple implementation - File first = file1.getParentFile(); - while (first != null) { - File second = file2.getParentFile(); - while (second != null) { - if (first.equals(second)) { - return first; - } - second = second.getParentFile(); - } - - first = first.getParentFile(); - } - } - return null; - } - - private static final String UTF_8 = "UTF-8"; //$NON-NLS-1$ - private static final String UTF_16 = "UTF_16"; //$NON-NLS-1$ - private static final String UTF_16LE = "UTF_16LE"; //$NON-NLS-1$ - - /** - * Returns the encoded String for the given file. This is usually the - * same as {@code Files.toString(file, Charsets.UTF8}, but if there's a UTF byte order mark - * (for UTF8, UTF_16 or UTF_16LE), use that instead. - * - * @param client the client to use for I/O operations - * @param file the file to read from - * @return the string - * @throws IOException if the file cannot be read properly - */ - @NonNull - public static String getEncodedString( - @NonNull LintClient client, - @NonNull File file) throws IOException { - byte[] bytes = client.readBytes(file); - if (endsWith(file.getName(), DOT_XML)) { - return PositionXmlParser.getXmlString(bytes); - } - - return getEncodedString(bytes); - } - - /** - * Returns the String corresponding to the given data. This is usually the - * same as {@code new String(data)}, but if there's a UTF byte order mark - * (for UTF8, UTF_16 or UTF_16LE), use that instead. - * <p> - * NOTE: For XML files, there is the additional complication that there - * could be a {@code encoding=} attribute in the prologue. For those files, - * use {@link PositionXmlParser#getXmlString(byte[])} instead. - * - * @param data the byte array to construct the string from - * @return the string - */ - @NonNull - public static String getEncodedString(@Nullable byte[] data) { - if (data == null) { - return ""; - } - - int offset = 0; - String defaultCharset = UTF_8; - String charset = null; - // Look for the byte order mark, to see if we need to remove bytes from - // the input stream (and to determine whether files are big endian or little endian) etc - // for files which do not specify the encoding. - // See http://unicode.org/faq/utf_bom.html#BOM for more. - if (data.length > 4) { - if (data[0] == (byte)0xef && data[1] == (byte)0xbb && data[2] == (byte)0xbf) { - // UTF-8 - defaultCharset = charset = UTF_8; - offset += 3; - } else if (data[0] == (byte)0xfe && data[1] == (byte)0xff) { - // UTF-16, big-endian - defaultCharset = charset = UTF_16; - offset += 2; - } else if (data[0] == (byte)0x0 && data[1] == (byte)0x0 - && data[2] == (byte)0xfe && data[3] == (byte)0xff) { - // UTF-32, big-endian - defaultCharset = charset = "UTF_32"; //$NON-NLS-1$ - offset += 4; - } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe - && data[2] == (byte)0x0 && data[3] == (byte)0x0) { - // UTF-32, little-endian. We must check for this *before* looking for - // UTF_16LE since UTF_32LE has the same prefix! - defaultCharset = charset = "UTF_32LE"; //$NON-NLS-1$ - offset += 4; - } else if (data[0] == (byte)0xff && data[1] == (byte)0xfe) { - // UTF-16, little-endian - defaultCharset = charset = UTF_16LE; - offset += 2; - } - } - int length = data.length - offset; - - // Guess encoding by searching for an encoding= entry in the first line. - boolean seenOddZero = false; - boolean seenEvenZero = false; - for (int lineEnd = offset; lineEnd < data.length; lineEnd++) { - if (data[lineEnd] == 0) { - if ((lineEnd - offset) % 2 == 0) { - seenEvenZero = true; - } else { - seenOddZero = true; - } - } else if (data[lineEnd] == '\n' || data[lineEnd] == '\r') { - break; - } - } - - if (charset == null) { - charset = seenOddZero ? UTF_16LE : seenEvenZero ? UTF_16 : UTF_8; - } - - String text = null; - try { - text = new String(data, offset, length, charset); - } catch (UnsupportedEncodingException e) { - try { - if (charset != defaultCharset) { - text = new String(data, offset, length, defaultCharset); - } - } catch (UnsupportedEncodingException u) { - // Just use the default encoding below - } - } - if (text == null) { - text = new String(data, offset, length); - } - return text; - } - - /** - * Returns true if the given class node represents a static inner class. - * - * @param classNode the inner class to be checked - * @return true if the class node represents an inner class that is static - */ - public static boolean isStaticInnerClass(@NonNull ClassNode classNode) { - // Note: We can't just filter out static inner classes like this: - // (classNode.access & Opcodes.ACC_STATIC) != 0 - // because the static flag only appears on methods and fields in the class - // file. Instead, look for the synthetic this pointer. - - @SuppressWarnings("rawtypes") // ASM API - List fieldList = classNode.fields; - for (Object f : fieldList) { - FieldNode field = (FieldNode) f; - if (field.name.startsWith("this$") && (field.access & Opcodes.ACC_SYNTHETIC) != 0) { - return false; - } - } - - return true; - } - - /** - * Returns the previous opcode prior to the given node, ignoring label and - * line number nodes - * - * @param node the node to look up the previous opcode for - * @return the previous opcode, or {@link Opcodes#NOP} if no previous node - * was found - */ - public static int getPrevOpcode(@NonNull AbstractInsnNode node) { - AbstractInsnNode prev = getPrevInstruction(node); - if (prev != null) { - return prev.getOpcode(); - } else { - return Opcodes.NOP; - } - } - - /** - * Returns the previous instruction prior to the given node, ignoring label - * and line number nodes. - * - * @param node the node to look up the previous instruction for - * @return the previous instruction, or null if no previous node was found - */ - @Nullable - public static AbstractInsnNode getPrevInstruction(@NonNull AbstractInsnNode node) { - AbstractInsnNode prev = node; - while (true) { - prev = prev.getPrevious(); - if (prev == null) { - return null; - } else { - int type = prev.getType(); - if (type != AbstractInsnNode.LINE && type != AbstractInsnNode.LABEL - && type != AbstractInsnNode.FRAME) { - return prev; - } - } - } - } - - /** - * Returns the next opcode after to the given node, ignoring label and line - * number nodes - * - * @param node the node to look up the next opcode for - * @return the next opcode, or {@link Opcodes#NOP} if no next node was found - */ - public static int getNextOpcode(@NonNull AbstractInsnNode node) { - AbstractInsnNode next = getNextInstruction(node); - if (next != null) { - return next.getOpcode(); - } else { - return Opcodes.NOP; - } - } - - /** - * Returns the next instruction after to the given node, ignoring label and - * line number nodes. - * - * @param node the node to look up the next node for - * @return the next instruction, or null if no next node was found - */ - @Nullable - public static AbstractInsnNode getNextInstruction(@NonNull AbstractInsnNode node) { - AbstractInsnNode next = node; - while (true) { - next = next.getNext(); - if (next == null) { - return null; - } else { - int type = next.getType(); - if (type != AbstractInsnNode.LINE && type != AbstractInsnNode.LABEL - && type != AbstractInsnNode.FRAME) { - return next; - } - } - } - } - - /** - * Returns true if the given directory represents an Android project - * directory. Note: This doesn't necessarily mean it's an Eclipse directory, - * only that it looks like it contains a logical Android project -- one - * including a manifest file, a resource folder, etc. - * - * @param dir the directory to check - * @return true if the directory looks like an Android project - */ - public static boolean isProjectDir(@NonNull File dir) { - boolean hasManifest = new File(dir, ANDROID_MANIFEST_XML).exists(); - if (hasManifest) { - // Special case: the bin/ folder can also contain a copy of the - // manifest file, but this is *not* a project directory - if (dir.getName().equals(BIN_FOLDER)) { - // ...unless of course it just *happens* to be a project named bin, in - // which case we peek at its parent to see if this is the case - dir = dir.getParentFile(); - if (dir != null && isProjectDir(dir)) { - // Yes, it's a bin/ directory inside a real project: ignore this dir - return false; - } - } - } else if (Project.isAospFrameworksProject(dir)) { - // Hardcoded AOSP support for the frameworks project - return true; - } - - return hasManifest; - } - - /** - * Look up the locale and region from the given parent folder name and - * return it as a combined string, such as "en", "en-rUS", etc, or null if - * no language is specified. - * - * @param folderName the folder name - * @return the locale+region string or null - */ - @Nullable - public static String getLocaleAndRegion(@NonNull String folderName) { - if (folderName.equals("values")) { //$NON-NLS-1$ - return null; - } - - String locale = null; - - for (String qualifier : Splitter.on('-').split(folderName)) { - int qualifierLength = qualifier.length(); - if (qualifierLength == 2) { - char first = qualifier.charAt(0); - char second = qualifier.charAt(1); - if (first >= 'a' && first <= 'z' && second >= 'a' && second <= 'z') { - locale = qualifier; - } - } else if (qualifierLength == 3 && qualifier.charAt(0) == 'r' && locale != null) { - char first = qualifier.charAt(1); - char second = qualifier.charAt(2); - if (first >= 'A' && first <= 'Z' && second >= 'A' && second <= 'Z') { - return locale + '-' + qualifier; - } - break; - } - } - - return locale; - } - - /** - * Returns true if the given class (specified by a fully qualified class - * name) name is imported in the given compilation unit either through a fully qualified - * import or by a wildcard import. - * - * @param compilationUnit the compilation unit - * @param fullyQualifiedName the fully qualified class name - * @return true if the given imported name refers to the given fully - * qualified name - */ - public static boolean isImported( - @NonNull lombok.ast.Node compilationUnit, - @NonNull String fullyQualifiedName) { - int dotIndex = fullyQualifiedName.lastIndexOf('.'); - int dotLength = fullyQualifiedName.length() - dotIndex; - - boolean imported = false; - for (lombok.ast.Node rootNode : compilationUnit.getChildren()) { - if (rootNode instanceof ImportDeclaration) { - ImportDeclaration importDeclaration = (ImportDeclaration) rootNode; - String fqn = importDeclaration.asFullyQualifiedName(); - if (fqn.equals(fullyQualifiedName)) { - return true; - } else if (fullyQualifiedName.regionMatches(dotIndex, fqn, - fqn.length() - dotLength, dotLength)) { - // This import is importing the class name using some other prefix, so there - // fully qualified class name cannot be imported under that name - return false; - } else if (importDeclaration.astStarImport() - && fqn.regionMatches(0, fqn, 0, dotIndex + 1)) { - imported = true; - // but don't break -- keep searching in case there's a non-wildcard - // import of the specific class name, e.g. if we're looking for - // android.content.SharedPreferences.Editor, don't match on the following: - // import android.content.SharedPreferences.*; - // import foo.bar.Editor; - } - } - } - - return imported; - } - - /** - * Returns the applicable build code (for - * {@code android.os.Build.VERSION_CODES}) for the corresponding API level, - * or null if it's unknown. - * - * @param api the API level to look up a version code for - * @return the corresponding build code field name, or null - */ - @Nullable - public static String getBuildCode(int api) { - // See http://developer.android.com/reference/android/os/Build.VERSION_CODES.html - switch (api) { - case 1: return "BASE"; //$NON-NLS-1$ - case 2: return "BASE_1_1"; //$NON-NLS-1$ - case 3: return "CUPCAKE"; //$NON-NLS-1$ - case 4: return "DONUT"; //$NON-NLS-1$ - case 5: return "ECLAIR"; //$NON-NLS-1$ - case 6: return "ECLAIR_0_1"; //$NON-NLS-1$ - case 7: return "ECLAIR_MR1"; //$NON-NLS-1$ - case 8: return "FROYO"; //$NON-NLS-1$ - case 9: return "GINGERBREAD"; //$NON-NLS-1$ - case 10: return "GINGERBREAD_MR1"; //$NON-NLS-1$ - case 11: return "HONEYCOMB"; //$NON-NLS-1$ - case 12: return "HONEYCOMB_MR1"; //$NON-NLS-1$ - case 13: return "HONEYCOMB_MR2"; //$NON-NLS-1$ - case 14: return "ICE_CREAM_SANDWICH"; //$NON-NLS-1$ - case 15: return "ICE_CREAM_SANDWICH_MR1"; //$NON-NLS-1$ - case 16: return "JELLY_BEAN"; //$NON-NLS-1$ - case 17: return "JELLY_BEAN_MR1"; //$NON-NLS-1$ - // If you add more versions here, also update AdtUtils#getAndroidName and - // SdkConstants#HIGHEST_KNOWN_API - } - - return null; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Location.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Location.java deleted file mode 100644 index 34116d0..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Location.java +++ /dev/null @@ -1,722 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.annotations.Beta; - -import java.io.File; - -/** - * Location information for a warning - * <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 class Location { - private static final String SUPER_KEYWORD = "super"; //$NON-NLS-1$ - - private final File mFile; - private final Position mStart; - private final Position mEnd; - private String mMessage; - private Location mSecondary; - private Object mClientData; - - /** - * (Private constructor, use one of the factory methods - * {@link Location#create(File)}, - * {@link Location#create(File, Position, Position)}, or - * {@link Location#create(File, String, int, int)}. - * <p> - * Constructs a new location range for the given file, from start to end. If - * the length of the range is not known, end may be null. - * - * @param file the associated file (but see the documentation for - * {@link #getFile()} for more information on what the file - * represents) - * @param start the starting position, or null - * @param end the ending position, or null - */ - protected Location(@NonNull File file, @Nullable Position start, @Nullable Position end) { - super(); - mFile = file; - mStart = start; - mEnd = end; - } - - /** - * Returns the file containing the warning. Note that the file *itself* may - * not yet contain the error. When editing a file in the IDE for example, - * the tool could generate warnings in the background even before the - * document is saved. However, the file is used as a identifying token for - * the document being edited, and the IDE integration can map this back to - * error locations in the editor source code. - * - * @return the file handle for the location - */ - @NonNull - public File getFile() { - return mFile; - } - - /** - * The start position of the range - * - * @return the start position of the range, or null - */ - @Nullable - public Position getStart() { - return mStart; - } - - /** - * The end position of the range - * - * @return the start position of the range, may be null for an empty range - */ - @Nullable - public Position getEnd() { - return mEnd; - } - - /** - * Returns a secondary location associated with this location (if - * applicable), or null. - * - * @return a secondary location or null - */ - @Nullable - public Location getSecondary() { - return mSecondary; - } - - /** - * Sets a secondary location for this location. - * - * @param secondary a secondary location associated with this location - */ - public void setSecondary(@Nullable Location secondary) { - mSecondary = secondary; - } - - /** - * Sets a custom message for this location. This is typically used for - * secondary locations, to describe the significance of this alternate - * location. For example, for a duplicate id warning, the primary location - * might say "This is a duplicate id", pointing to the second occurrence of - * id declaration, and then the secondary location could point to the - * original declaration with the custom message "Originally defined here". - * - * @param message the message to apply to this location - */ - public void setMessage(@NonNull String message) { - mMessage = message; - } - - /** - * Returns the custom message for this location, if any. This is typically - * used for secondary locations, to describe the significance of this - * alternate location. For example, for a duplicate id warning, the primary - * location might say "This is a duplicate id", pointing to the second - * occurrence of id declaration, and then the secondary location could point - * to the original declaration with the custom message - * "Originally defined here". - * - * @return the custom message for this location, or null - */ - @Nullable - public String getMessage() { - return mMessage; - } - - /** - * Sets the client data associated with this location. This is an optional - * field which can be used by the creator of the {@link Location} to store - * temporary state associated with the location. - * - * @param clientData the data to store with this location - */ - public void setClientData(@Nullable Object clientData) { - mClientData = clientData; - } - - /** - * Returns the client data associated with this location - an optional field - * which can be used by the creator of the {@link Location} to store - * temporary state associated with the location. - * - * @return the data associated with this location - */ - @Nullable - public Object getClientData() { - return mClientData; - } - - @Override - public String toString() { - return "Location [file=" + mFile + ", start=" + mStart + ", end=" + mEnd + ", message=" - + mMessage + ']'; - } - - /** - * Creates a new location for the given file - * - * @param file the file to create a location for - * @return a new location - */ - @NonNull - public static Location create(@NonNull File file) { - return new Location(file, null /*start*/, null /*end*/); - } - - /** - * Creates a new location for the given file and starting and ending - * positions. - * - * @param file the file containing the positions - * @param start the starting position - * @param end the ending position - * @return a new location - */ - @NonNull - public static Location create( - @NonNull File file, - @NonNull Position start, - @Nullable Position end) { - return new Location(file, start, end); - } - - /** - * Creates a new location for the given file, with the given contents, for - * the given offset range. - * - * @param file the file containing the location - * @param contents the current contents of the file - * @param startOffset the starting offset - * @param endOffset the ending offset - * @return a new location - */ - @NonNull - public static Location create( - @NonNull File file, - @Nullable String contents, - int startOffset, - int endOffset) { - if (startOffset < 0 || endOffset < startOffset) { - throw new IllegalArgumentException("Invalid offsets"); - } - - if (contents == null) { - return new Location(file, - new DefaultPosition(-1, -1, startOffset), - new DefaultPosition(-1, -1, endOffset)); - } - - int size = contents.length(); - endOffset = Math.min(endOffset, size); - startOffset = Math.min(startOffset, endOffset); - Position start = null; - int line = 0; - int lineOffset = 0; - char prev = 0; - for (int offset = 0; offset <= size; offset++) { - if (offset == startOffset) { - start = new DefaultPosition(line, offset - lineOffset, offset); - } - if (offset == endOffset) { - Position end = new DefaultPosition(line, offset - lineOffset, offset); - return new Location(file, start, end); - } - char c = contents.charAt(offset); - if (c == '\n') { - lineOffset = offset + 1; - if (prev != '\r') { - line++; - } - } else if (c == '\r') { - line++; - lineOffset = offset + 1; - } - prev = c; - } - return create(file); - } - - /** - * Creates a new location for the given file, with the given contents, for - * the given line number. - * - * @param file the file containing the location - * @param contents the current contents of the file - * @param line the line number (0-based) for the position - * @return a new location - */ - @NonNull - public static Location create(@NonNull File file, @NonNull String contents, int line) { - return create(file, contents, line, null, null, null); - } - - /** - * Creates a new location for the given file, with the given contents, for - * the given line number. - * - * @param file the file containing the location - * @param contents the current contents of the file - * @param line the line number (0-based) for the position - * @param patternStart an optional pattern to search for from the line - * match; if found, adjust the column and offsets to begin at the - * pattern start - * @param patternEnd an optional pattern to search for behind the start - * pattern; if found, adjust the end offset to match the end of - * the pattern - * @param hints optional additional information regarding the pattern search - * @return a new location - */ - @NonNull - public static Location create(@NonNull File file, @NonNull String contents, int line, - @Nullable String patternStart, @Nullable String patternEnd, - @Nullable SearchHints hints) { - int currentLine = 0; - int offset = 0; - while (currentLine < line) { - offset = contents.indexOf('\n', offset); - if (offset == -1) { - return create(file); - } - currentLine++; - offset++; - } - - if (line == currentLine) { - if (patternStart != null) { - SearchDirection direction = SearchDirection.NEAREST; - if (hints != null) { - direction = hints.mDirection; - } - - int index; - if (direction == SearchDirection.BACKWARD) { - index = findPreviousMatch(contents, offset, patternStart, hints); - line = adjustLine(contents, line, offset, index); - } else if (direction == SearchDirection.EOL_BACKWARD) { - int lineEnd = contents.indexOf('\n', offset); - if (lineEnd == -1) { - lineEnd = contents.length(); - } - - index = findPreviousMatch(contents, lineEnd, patternStart, hints); - line = adjustLine(contents, line, offset, index); - } else if (direction == SearchDirection.FORWARD) { - index = findNextMatch(contents, offset, patternStart, hints); - line = adjustLine(contents, line, offset, index); - } else { - assert direction == SearchDirection.NEAREST; - - int before = findPreviousMatch(contents, offset, patternStart, hints); - int after = findNextMatch(contents, offset, patternStart, hints); - - if (before == -1) { - index = after; - line = adjustLine(contents, line, offset, index); - } else if (after == -1) { - index = before; - line = adjustLine(contents, line, offset, index); - } else if (offset - before < after - offset) { - index = before; - line = adjustLine(contents, line, offset, index); - } else { - index = after; - line = adjustLine(contents, line, offset, index); - } - } - - if (index != -1) { - int lineStart = contents.lastIndexOf('\n', index); - if (lineStart == -1) { - lineStart = 0; - } else { - lineStart++; // was pointing to the previous line's CR, not line start - } - int column = index - lineStart; - if (patternEnd != null) { - int end = contents.indexOf(patternEnd, offset + patternStart.length()); - if (end != -1) { - return new Location(file, new DefaultPosition(line, column, index), - new DefaultPosition(line, -1, end + patternEnd.length())); - } - } else if (hints != null && (hints.isJavaSymbol() || hints.isWholeWord())) { - if (hints.isConstructor() && contents.startsWith(SUPER_KEYWORD, index)) { - patternStart = SUPER_KEYWORD; - } - return new Location(file, new DefaultPosition(line, column, index), - new DefaultPosition(line, column + patternStart.length(), - index + patternStart.length())); - } - return new Location(file, new DefaultPosition(line, column, index), - new DefaultPosition(line, column, index + patternStart.length())); - } - } - - Position position = new DefaultPosition(line, -1, offset); - return new Location(file, position, position); - } - - return create(file); - } - - private static int findPreviousMatch(@NonNull String contents, int offset, String pattern, - @Nullable SearchHints hints) { - while (true) { - int index = contents.lastIndexOf(pattern, offset); - if (index == -1) { - return -1; - } else { - if (isMatch(contents, index, pattern, hints)) { - return index; - } else { - offset = index - pattern.length(); - } - } - } - } - - private static int findNextMatch(@NonNull String contents, int offset, String pattern, - @Nullable SearchHints hints) { - int constructorIndex = -1; - if (hints != null && hints.isConstructor()) { - // Special condition: See if the call is referenced as "super" instead. - assert hints.isWholeWord(); - int index = contents.indexOf(SUPER_KEYWORD, offset); - if (index != -1 && isMatch(contents, index, SUPER_KEYWORD, hints)) { - constructorIndex = index; - } - } - - while (true) { - int index = contents.indexOf(pattern, offset); - if (index == -1) { - return constructorIndex; - } else { - if (isMatch(contents, index, pattern, hints)) { - if (constructorIndex != -1) { - return Math.min(constructorIndex, index); - } - return index; - } else { - offset = index + pattern.length(); - } - } - } - } - - private static boolean isMatch(@NonNull String contents, int offset, String pattern, - @Nullable SearchHints hints) { - if (!contents.startsWith(pattern, offset)) { - return false; - } - - if (hints != null) { - char prevChar = offset > 0 ? contents.charAt(offset - 1) : 0; - int lastIndex = offset + pattern.length() - 1; - char nextChar = lastIndex < contents.length() - 1 ? contents.charAt(lastIndex + 1) : 0; - - if (hints.isWholeWord() && (Character.isLetter(prevChar) - || Character.isLetter(nextChar))) { - return false; - - } - - if (hints.isJavaSymbol()) { - if (Character.isJavaIdentifierPart(prevChar) - || Character.isJavaIdentifierPart(nextChar)) { - return false; - } - - if (prevChar == '"') { - return false; - } - - // TODO: Additional validation to see if we're in a comment, string, etc. - // This will require lexing from the beginning of the buffer. - } - - if (hints.isConstructor() && SUPER_KEYWORD.equals(pattern)) { - // Only looking for super(), not super.x, so assert that the next - // non-space character is ( - int index = lastIndex + 1; - while (index < contents.length() - 1) { - char c = contents.charAt(index); - if (c == '(') { - break; - } else if (!Character.isWhitespace(c)) { - return false; - } - index++; - } - } - } - - return true; - } - - private static int adjustLine(String doc, int line, int offset, int newOffset) { - if (newOffset == -1) { - return line; - } - - if (newOffset < offset) { - return line - countLines(doc, newOffset, offset); - } else { - return line + countLines(doc, offset, newOffset); - } - } - - private static int countLines(String doc, int start, int end) { - int lines = 0; - for (int offset = start; offset < end; offset++) { - char c = doc.charAt(offset); - if (c == '\n') { - lines++; - } - } - - return lines; - } - - /** - * Reverses the secondary location list initiated by the given location - * - * @param location the first location in the list - * @return the first location in the reversed list - */ - public static Location reverse(Location location) { - Location next = location.getSecondary(); - location.setSecondary(null); - while (next != null) { - Location nextNext = next.getSecondary(); - next.setSecondary(location); - location = next; - next = nextNext; - } - - return location; - } - - /** - * A {@link Handle} is a reference to a location. The point of a location - * handle is to be able to create them cheaply, and then resolve them into - * actual locations later (if needed). This makes it possible to for example - * delay looking up line numbers, for locations that are offset based. - */ - public interface Handle { - /** - * Compute a full location for the given handle - * - * @return create a location for this handle - */ - @NonNull - Location resolve(); - - /** - * Sets the client data associated with this location. This is an optional - * field which can be used by the creator of the {@link Location} to store - * temporary state associated with the location. - * - * @param clientData the data to store with this location - */ - void setClientData(@Nullable Object clientData); - - /** - * Returns the client data associated with this location - an optional field - * which can be used by the creator of the {@link Location} to store - * temporary state associated with the location. - * - * @return the data associated with this location - */ - @Nullable - Object getClientData(); - } - - /** A default {@link Handle} implementation for simple file offsets */ - public static class DefaultLocationHandle implements Handle { - private final File mFile; - private final String mContents; - private final int mStartOffset; - private final int mEndOffset; - private Object mClientData; - - /** - * Constructs a new {@link DefaultLocationHandle} - * - * @param context the context pointing to the file and its contents - * @param startOffset the start offset within the file - * @param endOffset the end offset within the file - */ - public DefaultLocationHandle(@NonNull Context context, int startOffset, int endOffset) { - mFile = context.file; - mContents = context.getContents(); - mStartOffset = startOffset; - mEndOffset = endOffset; - } - - @Override - @NonNull - public Location resolve() { - return create(mFile, mContents, mStartOffset, mEndOffset); - } - - @Override - public void setClientData(@Nullable Object clientData) { - mClientData = clientData; - } - - @Override - @Nullable - public Object getClientData() { - return mClientData; - } - } - - /** - * Whether to look forwards, or backwards, or in both directions, when - * searching for a pattern in the source code to determine the right - * position range for a given symbol. - * <p> - * When dealing with bytecode for example, there are only line number entries - * within method bodies, so when searching for the method declaration, we should only - * search backwards from the first line entry in the method. - */ - public enum SearchDirection { - /** Only search forwards */ - FORWARD, - - /** Only search backwards */ - BACKWARD, - - /** Search backwards from the current end of line (normally it's the beginning of - * the current line) */ - EOL_BACKWARD, - - /** - * Search both forwards and backwards from the given line, and prefer - * the match that is closest - */ - NEAREST, - } - - /** - * Extra information pertaining to finding a symbol in a source buffer, - * used by {@link Location#create(File, String, int, String, String, SearchHints)} - */ - public static class SearchHints { - /** - * the direction to search for the nearest match in (provided - * {@code patternStart} is non null) - */ - @NonNull - private final SearchDirection mDirection; - - /** Whether the matched pattern should be a whole word */ - private boolean mWholeWord; - - /** - * Whether the matched pattern should be a Java symbol (so for example, - * a match inside a comment or string literal should not be used) - */ - private boolean mJavaSymbol; - - /** - * Whether the matched pattern corresponds to a constructor; if so, look for - * some other possible source aliases too, such as "super". - */ - private boolean mConstructor; - - private SearchHints(@NonNull SearchDirection direction) { - super(); - mDirection = direction; - } - - /** - * Constructs a new {@link SearchHints} object - * - * @param direction the direction to search in for the pattern - * @return a new @link SearchHints} object - */ - @NonNull - public static SearchHints create(@NonNull SearchDirection direction) { - return new SearchHints(direction); - } - - /** - * Indicates that pattern matches should apply to whole words only - - * @return this, for constructor chaining - */ - @NonNull - public SearchHints matchWholeWord() { - mWholeWord = true; - - return this; - } - - /** @return true if the pattern match should be for whole words only */ - public boolean isWholeWord() { - return mWholeWord; - } - - /** - * Indicates that pattern matches should apply to Java symbols only - * - * @return this, for constructor chaining - */ - @NonNull - public SearchHints matchJavaSymbol() { - mJavaSymbol = true; - mWholeWord = true; - - return this; - } - - /** @return true if the pattern match should be for Java symbols only */ - public boolean isJavaSymbol() { - return mJavaSymbol; - } - - /** - * Indicates that pattern matches should apply to constructors. If so, look for - * some other possible source aliases too, such as "super". - * - * @return this, for constructor chaining - */ - @NonNull - public SearchHints matchConstructor() { - mConstructor = true; - mWholeWord = true; - mJavaSymbol = true; - - return this; - } - - /** @return true if the pattern match should be for a constructor */ - public boolean isConstructor() { - return mConstructor; - } - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Position.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Position.java deleted file mode 100644 index 6407cc9..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Position.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.detector.api; - -import com.google.common.annotations.Beta; - -/** - * Information about a position in a file/document. - * <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 Position { - /** - * Returns the line number (0-based where the first line is line 0) - * - * @return the 0-based line number - */ - public abstract int getLine(); - - /** - * The character offset - * - * @return the 0-based character offset - */ - public abstract int getOffset(); - - /** - * Returns the column number (where the first character on the line is 0), - * or -1 if unknown - * - * @return the 0-based column number - */ - public abstract int getColumn(); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Project.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Project.java deleted file mode 100644 index 968b9b1..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Project.java +++ /dev/null @@ -1,929 +0,0 @@ -/* - * 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.detector.api; - -import static com.android.SdkConstants.ANDROID_LIBRARY; -import static com.android.SdkConstants.ANDROID_LIBRARY_REFERENCE_FORMAT; -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION; -import static com.android.SdkConstants.ATTR_PACKAGE; -import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION; -import static com.android.SdkConstants.PROGUARD_CONFIG; -import static com.android.SdkConstants.PROJECT_PROPERTIES; -import static com.android.SdkConstants.RES_FOLDER; -import static com.android.SdkConstants.TAG_USES_SDK; -import static com.android.SdkConstants.VALUE_TRUE; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.CircularDependencyException; -import com.android.tools.lint.client.api.Configuration; -import com.android.tools.lint.client.api.LintClient; -import com.android.tools.lint.client.api.SdkInfo; -import com.google.common.annotations.Beta; -import com.google.common.base.Charsets; -import com.google.common.io.Closeables; -import com.google.common.io.Files; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NodeList; - -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A project contains information about an Android project being scanned for - * Lint errors. - * <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 class Project { - private final LintClient mClient; - private final File mDir; - private final File mReferenceDir; - private Configuration mConfiguration; - private String mPackage; - private int mMinSdk = 1; - private int mTargetSdk = -1; - private int mBuildSdk = -1; - private boolean mLibrary; - private String mName; - private String mProguardPath; - private boolean mMergeManifests; - - /** The SDK info, if any */ - private SdkInfo mSdkInfo; - - /** - * If non null, specifies a non-empty list of specific files under this - * project which should be checked. - */ - private List<File> mFiles; - private List<File> mJavaSourceFolders; - private List<File> mJavaClassFolders; - private List<File> mJavaLibraries; - private List<Project> mDirectLibraries; - private List<Project> mAllLibraries; - private boolean mReportIssues = true; - - /** - * Creates a new {@link Project} for the given directory. - * - * @param client the tool running the lint check - * @param dir the root directory of the project - * @param referenceDir See {@link #getReferenceDir()}. - * @return a new {@link Project} - */ - @NonNull - public static Project create( - @NonNull LintClient client, - @NonNull File dir, - @NonNull File referenceDir) { - return new Project(client, dir, referenceDir); - } - - /** Creates a new Project. Use one of the factory methods to create. */ - private Project( - @NonNull LintClient client, - @NonNull File dir, - @NonNull File referenceDir) { - mClient = client; - mDir = dir; - mReferenceDir = referenceDir; - - try { - // Read properties file and initialize library state - Properties properties = new Properties(); - File propFile = new File(dir, PROJECT_PROPERTIES); - if (propFile.exists()) { - @SuppressWarnings("resource") // Eclipse doesn't know about Closeables.closeQuietly - BufferedInputStream is = new BufferedInputStream(new FileInputStream(propFile)); - try { - properties.load(is); - String value = properties.getProperty(ANDROID_LIBRARY); - mLibrary = VALUE_TRUE.equals(value); - mProguardPath = properties.getProperty(PROGUARD_CONFIG); - mMergeManifests = VALUE_TRUE.equals(properties.getProperty( - "manifestmerger.enabled")); //$NON-NLS-1$ - String target = properties.getProperty("target"); //$NON-NLS-1$ - if (target != null) { - int index = target.lastIndexOf('-'); - if (index == -1) { - index = target.lastIndexOf(':'); - } - if (index != -1) { - String versionString = target.substring(index + 1); - try { - mBuildSdk = Integer.parseInt(versionString); - } catch (NumberFormatException nufe) { - mClient.log(Severity.WARNING, null, - "Unexpected build target format: %1$s", target); - } - } - } - - for (int i = 1; i < 1000; i++) { - String key = String.format(ANDROID_LIBRARY_REFERENCE_FORMAT, i); - String library = properties.getProperty(key); - if (library == null || library.isEmpty()) { - // No holes in the numbering sequence is allowed - break; - } - - File libraryDir = new File(dir, library).getCanonicalFile(); - - if (mDirectLibraries == null) { - mDirectLibraries = new ArrayList<Project>(); - } - - // Adjust the reference dir to be a proper prefix path of the - // library dir - File libraryReferenceDir = referenceDir; - if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { - // Symlinks etc might have been resolved, so do those to - // the reference dir as well - libraryReferenceDir = libraryReferenceDir.getCanonicalFile(); - if (!libraryDir.getPath().startsWith(referenceDir.getPath())) { - File file = libraryReferenceDir; - while (file != null && !file.getPath().isEmpty()) { - if (libraryDir.getPath().startsWith(file.getPath())) { - libraryReferenceDir = file; - break; - } - file = file.getParentFile(); - } - } - } - - try { - Project libraryPrj = client.getProject(libraryDir, libraryReferenceDir); - mDirectLibraries.add(libraryPrj); - // By default, we don't report issues in inferred library projects. - // The driver will set report = true for those library explicitly - // requested. - libraryPrj.setReportIssues(false); - } catch (CircularDependencyException e) { - e.setProject(this); - e.setLocation(Location.create(propFile)); - throw e; - } - } - } finally { - Closeables.closeQuietly(is); - } - } - } catch (IOException ioe) { - client.log(ioe, "Initializing project state"); - } - - if (mDirectLibraries != null) { - mDirectLibraries = Collections.unmodifiableList(mDirectLibraries); - } else { - mDirectLibraries = Collections.emptyList(); - } - } - - @Override - public String toString() { - return "Project [dir=" + mDir + ']'; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((mDir == null) ? 0 : mDir.hashCode()); - return result; - } - - @Override - public boolean equals(@Nullable Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Project other = (Project) obj; - if (mDir == null) { - if (other.mDir != null) - return false; - } else if (!mDir.equals(other.mDir)) - return false; - return true; - } - - /** - * Adds the given file to the list of files which should be checked in this - * project. If no files are added, the whole project will be checked. - * - * @param file the file to be checked - */ - public void addFile(@NonNull File file) { - if (mFiles == null) { - mFiles = new ArrayList<File>(); - } - mFiles.add(file); - } - - /** - * The list of files to be checked in this project. If null, the whole - * project should be checked. - * - * @return the subset of files to be checked, or null for the whole project - */ - @Nullable - public List<File> getSubset() { - return mFiles; - } - - /** - * Returns the list of source folders for Java source files - * - * @return a list of source folders to search for .java files - */ - @NonNull - public List<File> getJavaSourceFolders() { - if (mJavaSourceFolders == null) { - if (isAospFrameworksProject(mDir)) { - return Collections.singletonList(new File(mDir, "java")); //$NON-NLS-1$ - } - if (isAospBuildEnvironment()) { - String top = getAospTop(); - if (mDir.getAbsolutePath().startsWith(top)) { - mJavaSourceFolders = getAospJavaSourcePath(); - return mJavaSourceFolders; - } - } - - mJavaSourceFolders = mClient.getJavaSourceFolders(this); - } - - return mJavaSourceFolders; - } - - /** - * Returns the list of output folders for class files - * @return a list of output folders to search for .class files - */ - @NonNull - public List<File> getJavaClassFolders() { - if (mJavaClassFolders == null) { - if (isAospFrameworksProject(mDir)) { - File top = mDir.getParentFile().getParentFile().getParentFile(); - if (top != null) { - File out = new File(top, "out"); - if (out.exists()) { - String relative = - "target/common/obj/JAVA_LIBRARIES/framework_intermediates/classes.jar"; - File jar = new File(out, relative.replace('/', File.separatorChar)); - if (jar.exists()) { - mJavaClassFolders = Collections.singletonList(jar); - return mJavaClassFolders; - } - } - } - } - if (isAospBuildEnvironment()) { - String top = getAospTop(); - if (mDir.getAbsolutePath().startsWith(top)) { - mJavaClassFolders = getAospJavaClassPath(); - return mJavaClassFolders; - } - } - - mJavaClassFolders = mClient.getJavaClassFolders(this); - } - return mJavaClassFolders; - } - - /** - * Returns the list of Java libraries (typically .jar files) that this - * project depends on. Note that this refers to jar libraries, not Android - * library projects which are processed in a separate pass with their own - * source and class folders. - * - * @return a list of .jar files (or class folders) that this project depends - * on. - */ - @NonNull - public List<File> getJavaLibraries() { - if (mJavaLibraries == null) { - // AOSP builds already merge libraries and class folders into - // the single classes.jar file, so these have already been processed - // in getJavaClassFolders. - - mJavaLibraries = mClient.getJavaLibraries(this); - } - - return mJavaLibraries; - } - - /** - * Returns the resource folder. - * - * @return a file pointing to the resource folder, or null if the project - * does not contain any resources - */ - @NonNull - public List<File> getResourceFolders() { - List<File> folders = mClient.getResourceFolders(this); - - if (folders.size() == 1 && isAospFrameworksProject(mDir)) { - // No manifest file for this project: just init the manifest values here - mMinSdk = mTargetSdk = SdkConstants.HIGHEST_KNOWN_API; - File folder = new File(folders.get(0), RES_FOLDER); - if (!folder.exists()) { - folders = Collections.emptyList(); - } - } - - return folders; - } - - /** - * Returns the relative path of a given file relative to the user specified - * directory (which is often the project directory but sometimes a higher up - * directory when a directory tree is being scanned - * - * @param file the file under this project to check - * @return the path relative to the reference directory (often the project directory) - */ - @NonNull - public String getDisplayPath(@NonNull File file) { - String path = file.getPath(); - String referencePath = mReferenceDir.getPath(); - if (path.startsWith(referencePath)) { - int length = referencePath.length(); - if (path.length() > length && path.charAt(length) == File.separatorChar) { - length++; - } - - return path.substring(length); - } - - return path; - } - - /** - * Returns the relative path of a given file within the current project. - * - * @param file the file under this project to check - * @return the path relative to the project - */ - @NonNull - public String getRelativePath(@NonNull File file) { - String path = file.getPath(); - String referencePath = mDir.getPath(); - if (path.startsWith(referencePath)) { - int length = referencePath.length(); - if (path.length() > length && path.charAt(length) == File.separatorChar) { - length++; - } - - return path.substring(length); - } - - return path; - } - - /** - * Returns the project root directory - * - * @return the dir - */ - @NonNull - public File getDir() { - return mDir; - } - - /** - * Returns the original user supplied directory where the lint search - * started. For example, if you run lint against {@code /tmp/foo}, and it - * finds a project to lint in {@code /tmp/foo/dev/src/project1}, then the - * {@code dir} is {@code /tmp/foo/dev/src/project1} and the - * {@code referenceDir} is {@code /tmp/foo/}. - * - * @return the reference directory, never null - */ - @NonNull - public File getReferenceDir() { - return mReferenceDir; - } - - /** - * Gets the configuration associated with this project - * - * @return the configuration associated with this project - */ - @NonNull - public Configuration getConfiguration() { - if (mConfiguration == null) { - mConfiguration = mClient.getConfiguration(this); - } - return mConfiguration; - } - - /** - * Returns the application package specified by the manifest - * - * @return the application package, or null if unknown - */ - @Nullable - public String getPackage() { - //assert !mLibrary; // Should call getPackage on the master project, not the library - // Assertion disabled because you might be running lint on a standalone library project. - - return mPackage; - } - - /** - * Returns the minimum API level requested by the manifest, or -1 if not - * specified - * - * @return the minimum API level or -1 if unknown - */ - public int getMinSdk() { - //assert !mLibrary; // Should call getMinSdk on the master project, not the library - // Assertion disabled because you might be running lint on a standalone library project. - - return mMinSdk; - } - - /** - * Returns the target API level specified by the manifest, or -1 if not - * specified - * - * @return the target API level or -1 if unknown - */ - public int getTargetSdk() { - //assert !mLibrary; // Should call getTargetSdk on the master project, not the library - // Assertion disabled because you might be running lint on a standalone library project. - - return mTargetSdk; - } - - /** - * Returns the target API used to build the project, or -1 if not known - * - * @return the build target API or -1 if unknown - */ - public int getBuildSdk() { - return mBuildSdk; - } - - /** - * Initialized the manifest state from the given manifest model - * - * @param document the DOM document for the manifest XML document - */ - public void readManifest(@NonNull Document document) { - Element root = document.getDocumentElement(); - if (root == null) { - return; - } - - mPackage = root.getAttribute(ATTR_PACKAGE); - - // Initialize minSdk and targetSdk - NodeList usesSdks = root.getElementsByTagName(TAG_USES_SDK); - if (usesSdks.getLength() > 0) { - Element element = (Element) usesSdks.item(0); - - String minSdk = null; - if (element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { - minSdk = element.getAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); - } - if (minSdk != null) { - try { - mMinSdk = Integer.valueOf(minSdk); - } catch (NumberFormatException e) { - mMinSdk = 1; - } - } - - String targetSdk = null; - if (element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { - targetSdk = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); - } else if (minSdk != null) { - targetSdk = minSdk; - } - if (targetSdk != null) { - try { - mTargetSdk = Integer.valueOf(targetSdk); - } catch (NumberFormatException e) { - // TODO: Handle codenames? - mTargetSdk = -1; - } - } - } else if (isAospBuildEnvironment()) { - extractAospMinSdkVersion(); - } - } - - /** - * Returns true if this project is an Android library project - * - * @return true if this project is an Android library project - */ - public boolean isLibrary() { - return mLibrary; - } - - /** - * Returns the list of library projects referenced by this project - * - * @return the list of library projects referenced by this project, never - * null - */ - @NonNull - public List<Project> getDirectLibraries() { - return mDirectLibraries; - } - - /** - * Returns the transitive closure of the library projects for this project - * - * @return the transitive closure of the library projects for this project - */ - @NonNull - public List<Project> getAllLibraries() { - if (mAllLibraries == null) { - if (mDirectLibraries.isEmpty()) { - return mDirectLibraries; - } - - List<Project> all = new ArrayList<Project>(); - addLibraryProjects(all); - mAllLibraries = all; - } - - return mAllLibraries; - } - - /** - * Adds this project's library project and their library projects - * recursively into the given collection of projects - * - * @param collection the collection to add the projects into - */ - private void addLibraryProjects(@NonNull Collection<Project> collection) { - for (Project library : mDirectLibraries) { - collection.add(library); - // Recurse - library.addLibraryProjects(collection); - } - } - - /** - * Gets the SDK info for the current project. - * - * @return the SDK info for the current project, never null - */ - @NonNull - public SdkInfo getSdkInfo() { - if (mSdkInfo == null) { - mSdkInfo = mClient.getSdkInfo(this); - } - - return mSdkInfo; - } - - /** - * Gets the path to the manifest file in this project, if it exists - * - * @return the path to the manifest file, or null if it does not exist - */ - @Nullable - public File getManifestFile() { - File manifestFile = new File(mDir, ANDROID_MANIFEST_XML); - if (manifestFile.exists()) { - return manifestFile; - } - - return null; - } - - /** - * Returns the proguard path configured for this project, or null if ProGuard is - * not configured. - * - * @return the proguard path, or null - */ - @Nullable - public String getProguardPath() { - return mProguardPath; - } - - /** - * Returns the name of the project - * - * @return the name of the project, never null - */ - @NonNull - public String getName() { - if (mName == null) { - mName = mClient.getProjectName(this); - } - - return mName; - } - - /** - * Sets the name of the project - * - * @param name the name of the project, never null - */ - public void setName(@NonNull String name) { - assert !name.isEmpty(); - mName = name; - } - - /** - * Sets whether lint should report issues in this project. See - * {@link #getReportIssues()} for a full description of what that means. - * - * @param reportIssues whether lint should report issues in this project - */ - public void setReportIssues(boolean reportIssues) { - mReportIssues = reportIssues; - } - - /** - * Returns whether lint should report issues in this project. - * <p> - * If a user specifies a project and its library projects for analysis, then - * those library projects are all "included", and all errors found in all - * the projects are reported. But if the user is only running lint on the - * main project, we shouldn't report errors in any of the library projects. - * We still need to <b>consider</b> them for certain types of checks, such - * as determining whether resources found in the main project are unused, so - * the detectors must still get a chance to look at these projects. The - * {@code #getReportIssues()} attribute is used for this purpose. - * - * @return whether lint should report issues in this project - */ - public boolean getReportIssues() { - return mReportIssues; - } - - /** - * Sets whether manifest merging is in effect. - * - * @param merging whether manifest merging is in effect - */ - public void setMergingManifests(boolean merging) { - mMergeManifests = merging; - } - - /** - * Returns whether manifest merging is in effect - * - * @return true if manifests in library projects should be merged into main projects - */ - public boolean isMergingManifests() { - return mMergeManifests; - } - - - // --------------------------------------------------------------------------- - // Support for running lint on the AOSP source tree itself - - private static Boolean sAospBuild; - - /** Is lint running in an AOSP build environment */ - private static boolean isAospBuildEnvironment() { - if (sAospBuild == null) { - sAospBuild = getAospTop() != null; - } - - return sAospBuild.booleanValue(); - } - - /** - * Is this the frameworks AOSP project? Needs some hardcoded support since - * it doesn't have a manifest file, etc. - * - * @param dir the project directory to check - * @return true if this looks like the frameworks/base/core project - */ - static boolean isAospFrameworksProject(@NonNull File dir) { - if (!dir.getPath().endsWith("core")) { //$NON-NLS-1$ - return false; - } - - File parent = dir.getParentFile(); - if (parent == null || !parent.getName().equals("base")) { //$NON-NLS-1$ - return false; - } - - parent = parent.getParentFile(); - if (parent == null || !parent.getName().equals("frameworks")) { //$NON-NLS-1$ - return false; - } - - return true; - } - - /** Get the root AOSP dir, if any */ - private static String getAospTop() { - return System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ - } - - /** Get the host out directory in AOSP, if any */ - private static String getAospHostOut() { - return System.getenv("ANDROID_HOST_OUT"); //$NON-NLS-1$ - } - - /** Get the product out directory in AOSP, if any */ - private static String getAospProductOut() { - return System.getenv("ANDROID_PRODUCT_OUT"); //$NON-NLS-1$ - } - - private List<File> getAospJavaSourcePath() { - List<File> sources = new ArrayList<File>(2); - // Normal sources - File src = new File(mDir, "src"); //$NON-NLS-1$ - if (src.exists()) { - sources.add(src); - } - - // Generates sources - for (File dir : getIntermediateDirs()) { - File classes = new File(dir, "src"); //$NON-NLS-1$ - if (classes.exists()) { - sources.add(classes); - } - } - - if (sources.isEmpty()) { - mClient.log(null, - "Warning: Could not find sources or generated sources for project %1$s", - getName()); - } - - return sources; - } - - private List<File> getAospJavaClassPath() { - List<File> classDirs = new ArrayList<File>(1); - - for (File dir : getIntermediateDirs()) { - File classes = new File(dir, "classes"); //$NON-NLS-1$ - if (classes.exists()) { - classDirs.add(classes); - } else { - classes = new File(dir, "classes.jar"); //$NON-NLS-1$ - if (classes.exists()) { - classDirs.add(classes); - } - } - } - - if (classDirs.isEmpty()) { - mClient.log(null, - "No bytecode found: Has the project been built? (%1$s)", getName()); - } - - return classDirs; - } - - /** Find the _intermediates directories for a given module name */ - private List<File> getIntermediateDirs() { - // See build/core/definitions.mk and in particular the "intermediates-dir-for" definition - List<File> intermediates = new ArrayList<File>(); - - // TODO: Look up the module name, e.g. LOCAL_MODULE. However, - // some Android.mk files do some complicated things with it - and most - // projects use the same module name as the directory name. - String moduleName = mDir.getName(); - - String top = getAospTop(); - final String[] outFolders = new String[] { - top + "/out/host/common/obj", //$NON-NLS-1$ - top + "/out/target/common/obj", //$NON-NLS-1$ - getAospHostOut() + "/obj", //$NON-NLS-1$ - getAospProductOut() + "/obj" //$NON-NLS-1$ - }; - final String[] moduleClasses = new String[] { - "APPS", //$NON-NLS-1$ - "JAVA_LIBRARIES", //$NON-NLS-1$ - }; - - for (String out : outFolders) { - assert new File(out.replace('/', File.separatorChar)).exists() : out; - for (String moduleClass : moduleClasses) { - String path = out + '/' + moduleClass + '/' + moduleName - + "_intermediates"; //$NON-NLS-1$ - File file = new File(path.replace('/', File.separatorChar)); - if (file.exists()) { - intermediates.add(file); - } - } - } - - return intermediates; - } - - private void extractAospMinSdkVersion() { - // Is the SDK level specified by a Makefile? - boolean found = false; - File makefile = new File(mDir, "Android.mk"); //$NON-NLS-1$ - if (makefile.exists()) { - try { - List<String> lines = Files.readLines(makefile, Charsets.UTF_8); - Pattern p = Pattern.compile("LOCAL_SDK_VERSION\\s*:=\\s*(.*)"); //$NON-NLS-1$ - for (String line : lines) { - line = line.trim(); - Matcher matcher = p.matcher(line); - if (matcher.matches()) { - found = true; - String version = matcher.group(1); - if (version.equals("current")) { //$NON-NLS-1$ - mMinSdk = findCurrentAospVersion(); - } else { - try { - mMinSdk = Integer.valueOf(version); - } catch (NumberFormatException e) { - // Codename - just use current - mMinSdk = findCurrentAospVersion(); - } - } - break; - } - } - } catch (IOException ioe) { - mClient.log(ioe, null); - } - } - - if (!found) { - mMinSdk = findCurrentAospVersion(); - } - } - - /** Cache for {@link #findCurrentAospVersion()} */ - private static int sCurrentVersion; - - /** In an AOSP build environment, identify the currently built image version, if available */ - private static int findCurrentAospVersion() { - if (sCurrentVersion < 1) { - File apiDir = new File(getAospTop(), "frameworks/base/api" //$NON-NLS-1$ - .replace('/', File.separatorChar)); - File[] apiFiles = apiDir.listFiles(); - if (apiFiles == null) { - sCurrentVersion = 1; - return sCurrentVersion; - } - int max = 1; - for (File apiFile : apiFiles) { - String name = apiFile.getName(); - int index = name.indexOf('.'); - if (index > 0) { - String base = name.substring(0, index); - if (Character.isDigit(base.charAt(0))) { - try { - int version = Integer.parseInt(base); - if (version > max) { - max = version; - } - } catch (NumberFormatException nufe) { - // pass - } - } - } - } - sCurrentVersion = max; - } - - return sCurrentVersion; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java deleted file mode 100644 index 68685c6..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/ResourceXmlDetector.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.google.common.annotations.Beta; - -import java.io.File; - -/** - * Specialized detector intended for XML resources. Detectors that apply to XML - * resources should extend this detector instead since it provides special - * iteration hooks that are more efficient. - * <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 ResourceXmlDetector extends Detector implements Detector.XmlScanner { - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return LintUtils.isXmlFile(file); - } - - /** - * Returns whether this detector applies to the given folder type. This - * allows the detectors to be pruned from iteration, so for example when we - * are analyzing a string value file we don't need to look up detectors - * related to layout. - * - * @param folderType the folder type to be visited - * @return true if this detector can apply to resources in folders of the - * given type - */ - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return true; - } - - @Override - public void run(@NonNull Context context) { - // The infrastructure should never call this method on an xml detector since - // it will run the various visitors instead - assert false; - } -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Scope.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Scope.java deleted file mode 100644 index 0e33f1b..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Scope.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.google.common.annotations.Beta; - -import java.util.EnumSet; - -/** - * The scope of a detector is the set of files a detector must consider when - * performing its analysis. This can be used to determine when issues are - * potentially obsolete, whether a detector should re-run on a file save, etc. - * <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 enum Scope { - /** - * The analysis only considers a single XML resource file at a time. - * <p> - * Issues which are only affected by a single resource file can be checked - * for incrementally when a file is edited. - */ - RESOURCE_FILE, - - /** - * The analysis considers <b>all</b> the resource file. This scope must not - * be used in conjunction with {@link #RESOURCE_FILE}; an issue scope is - * either considering just a single resource file or all the resources, not - * both. - */ - ALL_RESOURCE_FILES, - - /** - * The analysis only considers a single Java source file at a time. - * <p> - * Issues which are only affected by a single Java source file can be - * checked for incrementally when a Java source file is edited. - */ - JAVA_FILE, - - /** - * The analysis considers <b>all</b> the Java source files together. - * <p> - * This flag is mutually exclusive with {@link #JAVA_FILE}. - */ - ALL_JAVA_FILES, - - /** - * The analysis only considers a single Java class file at a time. - * <p> - * Issues which are only affected by a single Java class file can be checked - * for incrementally when a Java source file is edited and then recompiled. - */ - CLASS_FILE, - - /** - * The analysis considers <b>all</b> the Java class files together. - * <p> - * This flag is mutually exclusive with {@link #CLASS_FILE}. - */ - ALL_CLASS_FILES, - - /** The analysis considers the manifest file */ - MANIFEST, - - /** The analysis considers the Proguard configuration file */ - PROGUARD_FILE, - - /** - * The analysis considers classes in the libraries for this project. These - * will be analyzed before the classes themselves. - */ - JAVA_LIBRARIES, - - /** - * Scope for other files. Issues that specify a custom scope will be called unconditionally. - * This will call {@link Detector#run(Context)}} on the detectors unconditionally. - */ - OTHER; - - /** - * Returns true if the given scope set corresponds to scanning a single file - * rather than a whole project - * - * @param scopes the scope set to check - * @return true if the scope set references a single file - */ - public static boolean checkSingleFile(@NonNull EnumSet<Scope> scopes) { - int size = scopes.size(); - if (size == 2) { - // When single checking a Java source file, we check both its Java source - // and the associated class files - return scopes.contains(JAVA_FILE) && scopes.contains(CLASS_FILE); - } else { - return size == 1 && - (scopes.contains(JAVA_FILE) - || scopes.contains(CLASS_FILE) - || scopes.contains(RESOURCE_FILE) - || scopes.contains(PROGUARD_FILE) - || scopes.contains(MANIFEST)); - } - } - - /** - * Returns the intersection of two scope sets - * - * @param scope1 the first set to intersect - * @param scope2 the second set to intersect - * @return the intersection of the two sets - */ - @NonNull - public static EnumSet<Scope> intersect( - @NonNull EnumSet<Scope> scope1, - @NonNull EnumSet<Scope> scope2) { - EnumSet<Scope> scope = EnumSet.copyOf(scope1); - scope.retainAll(scope2); - - return scope; - } - - /** All scopes: running lint on a project will check these scopes */ - public static final EnumSet<Scope> ALL = EnumSet.allOf(Scope.class); - /** Scope-set used for detectors which are affected by a single resource file */ - public static final EnumSet<Scope> RESOURCE_FILE_SCOPE = EnumSet.of(RESOURCE_FILE); - /** Scope-set used for detectors which scan all resources */ - public static final EnumSet<Scope> ALL_RESOURCES_SCOPE = EnumSet.of(ALL_RESOURCE_FILES); - /** Scope-set used for detectors which are affected by a single Java source file */ - public static final EnumSet<Scope> JAVA_FILE_SCOPE = EnumSet.of(JAVA_FILE); - /** Scope-set used for detectors which are affected by a single Java class file */ - public static final EnumSet<Scope> CLASS_FILE_SCOPE = EnumSet.of(CLASS_FILE); - /** Scope-set used for detectors which are affected by the manifest only */ - public static final EnumSet<Scope> MANIFEST_SCOPE = EnumSet.of(MANIFEST); - /** Scope-set used for detectors which correspond to some other context */ - public static final EnumSet<Scope> OTHER_SCOPE = EnumSet.of(OTHER); -} diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Severity.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Severity.java deleted file mode 100644 index f74e6b5..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Severity.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.google.common.annotations.Beta; - -/** - * Severity of an issue found by lint - * <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 enum Severity { - /** - * Fatal: Use sparingly because a warning marked as fatal will be - * considered critical and will abort Export APK etc in ADT - */ - @NonNull - FATAL("Fatal"), - - /** - * Errors: The issue is known to be a real error that must be addressed. - */ - @NonNull - ERROR("Error"), - - /** - * Warning: Probably a problem. - */ - @NonNull - WARNING("Warning"), - - /** - * Information only: Might not be a problem, but the check has found - * something interesting to say about the code. - */ - @NonNull - INFORMATIONAL("Information"), - - /** - * Ignore: The user doesn't want to see this issue - */ - @NonNull - IGNORE("Ignore"); - - @NonNull - private final String mDisplay; - - Severity(@NonNull String display) { - mDisplay = display; - } - - /** - * Returns a description of this severity suitable for display to the user - * - * @return a description of the severity - */ - @NonNull - public String getDescription() { - return mDisplay; - } -}
\ No newline at end of file diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Speed.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Speed.java deleted file mode 100644 index c68dab0..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/Speed.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.google.common.annotations.Beta; - -/** - * Enum which describes the different computation speeds of various detectors - * <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 enum Speed { - /** The detector can run very quickly */ - FAST("Fast"), - - /** The detector runs reasonably fast */ - NORMAL("Normal"), - - /** The detector might take a long time to run */ - SLOW("Slow"); - - private final String mDisplayName; - - Speed(@NonNull String displayName) { - mDisplayName = displayName; - } - - /** - * Returns the user-visible description of the speed of the given - * detector - * - * @return the description of the speed to display to the user - */ - @NonNull - public String getDisplayName() { - return mDisplayName; - } -}
\ No newline at end of file diff --git a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/XmlContext.java b/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/XmlContext.java deleted file mode 100644 index 34d6816..0000000 --- a/lint/libs/lint_api/src/main/java/com/android/tools/lint/detector/api/XmlContext.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * 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.detector.api; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.IDomParser; -import com.android.tools.lint.client.api.LintDriver; -import com.google.common.annotations.Beta; - -import org.w3c.dom.Document; -import org.w3c.dom.Node; - -import java.io.File; - -/** - * A {@link Context} used when checking XML files. - * <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 class XmlContext extends Context { - /** The XML parser */ - public IDomParser parser; - /** The XML document */ - public Document document; - private final ResourceFolderType mFolderType; - - /** - * Construct a new {@link XmlContext} - * - * @param driver the driver running through the checks - * @param project the project containing the file being checked - * @param main the main project if this project is a library project, or - * null if this is not a library project. The main project is - * the root project of all library projects, not necessarily the - * directly including project. - * @param file the file being checked - * @param folderType the {@link ResourceFolderType} of this file, if any - */ - public XmlContext( - @NonNull LintDriver driver, - @NonNull Project project, - @Nullable Project main, - @NonNull File file, - @Nullable ResourceFolderType folderType) { - super(driver, project, main, file); - mFolderType = folderType; - } - - /** - * Returns the location for the given node, which may be an element or an attribute. - * - * @param node the node to look up the location for - * @return the location for the node - */ - @NonNull - public Location getLocation(@NonNull Node node) { - if (parser != null) { - return parser.getLocation(this, node); - } - - return Location.create(file); - } - - /** - * Creates a new location within an XML text node - * - * @param textNode the text node - * @param begin the start offset within the text node (inclusive) - * @param end the end offset within the text node (exclusive) - * @return a new location - */ - @NonNull - public Location getLocation(@NonNull Node textNode, int begin, int end) { - assert textNode.getNodeType() == Node.TEXT_NODE; - if (parser != null) { - return parser.getLocation(this, textNode, begin, end); - } - - return Location.create(file); - } - - - /** - * Reports an issue applicable to a given DOM node. The DOM node is used as the - * scope to check for suppress lint annotations. - * - * @param issue the issue to report - * @param scope the DOM node scope the error applies to. The lint infrastructure - * will check whether there are suppress directives on this node (or its enclosing - * nodes) and if so suppress the warning without involving the client. - * @param location the location of the issue, or null if not known - * @param message the message for this warning - * @param data any associated data, or null - */ - public void report( - @NonNull Issue issue, - @Nullable Node scope, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - if (scope != null && mDriver.isSuppressed(issue, scope)) { - return; - } - super.report(issue, location, message, data); - } - - @Override - public void report( - @NonNull Issue issue, - @Nullable Location location, - @NonNull String message, - @Nullable Object data) { - // Warn if clients use the non-scoped form? No, there are cases where an - // XML detector's error isn't applicable to one particular location (or it's - // not feasible to compute it cheaply) - //mDriver.getClient().log(null, "Warning: Issue " + issue - // + " was reported without a scope node: Can't be suppressed."); - - // For now just check the document root itself - if (document != null && mDriver.isSuppressed(issue, document)) { - return; - } - - super.report(issue, location, message, data); - } - - /** - * Returns the resource folder type of this XML file, if any. - * - * @return the resource folder type or null - */ - @Nullable - public ResourceFolderType getResourceFolderType() { - return mFolderType; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AccessibilityDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AccessibilityDetector.java deleted file mode 100644 index 4d069cc..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AccessibilityDetector.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION; -import static com.android.SdkConstants.ATTR_HINT; -import static com.android.SdkConstants.ATTR_IMPORTANT_FOR_ACCESSIBILITY; -import static com.android.SdkConstants.IMAGE_BUTTON; -import static com.android.SdkConstants.IMAGE_VIEW; -import static com.android.SdkConstants.VALUE_NO; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -/** - * Check which looks for accessibility problems like missing content descriptions - * <p> - * TODO: Resolve styles and don't warn where styles are defining the content description - * (though this seems unusual; content descriptions are not typically generic enough to - * put in styles) - */ -public class AccessibilityDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ContentDescription", //$NON-NLS-1$ - "Ensures that image widgets provide a contentDescription", - "Non-textual widgets like ImageViews and ImageButtons should use the " + - "`contentDescription` attribute to specify a textual description of " + - "the widget such that screen readers and other accessibility tools " + - "can adequately describe the user interface.\n" + - "\n" + - "Note that elements in application screens that are purely decorative " + - "and do not provide any content or enable a user action should not " + - "have accessibility content descriptions. In this case, just suppress the " + - "lint warning with a tools:ignore=\"ContentDescription\" attribute.\n" + - "\n" + - "Note that for text fields, you should not set both the `hint` and the " + - "`contentDescription` attributes since the hint will never be shown. Just " + - "set the `hint`. See " + - "http://developer.android.com/guide/topics/ui/accessibility/checklist.html#special-cases.", - - Category.A11Y, - 3, - Severity.WARNING, - AccessibilityDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link AccessibilityDetector} */ - public AccessibilityDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - IMAGE_BUTTON, - IMAGE_VIEW - ); - } - - @Override - @Nullable - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_CONTENT_DESCRIPTION); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - Element element = attribute.getOwnerElement(); - if (element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) { - context.report(ISSUE, element, context.getLocation(attribute), - "Do not set both contentDescription and hint: the contentDescription " + - "will mask the hint", null); - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (!element.hasAttributeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION)) { - // Ignore views that are explicitly not important for accessibility - if (VALUE_NO.equals(element.getAttributeNS(ANDROID_URI, - ATTR_IMPORTANT_FOR_ACCESSIBILITY))) { - return; - } - context.report(ISSUE, element, context.getLocation(element), - "[Accessibility] Missing contentDescription attribute on image", null); - } else { - Attr attributeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_CONTENT_DESCRIPTION); - String attribute = attributeNode.getValue(); - if (attribute.isEmpty() || attribute.equals("TODO")) { //$NON-NLS-1$ - context.report(ISSUE, attributeNode, context.getLocation(attributeNode), - "[Accessibility] Empty contentDescription attribute on image", null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AlwaysShowActionDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AlwaysShowActionDetector.java deleted file mode 100644 index 34a3f10..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AlwaysShowActionDetector.java +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright (C) 2012 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.ATTR_SHOW_AS_ACTION; -import static com.android.SdkConstants.VALUE_ALWAYS; -import static com.android.SdkConstants.VALUE_IF_ROOM; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector.JavaScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Attr; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.Node; -import lombok.ast.Select; - -/** - * Check which looks for usage of showAsAction="always" in menus (or - * MenuItem.SHOW_AS_ACTION_ALWAYS in code), which is usually a style guide violation. - * (Use ifRoom instead). - */ -public class AlwaysShowActionDetector extends ResourceXmlDetector implements JavaScanner { - - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "AlwaysShowAction", //$NON-NLS-1$ - "Checks for uses of showAsAction=\"always\" and suggests showAsAction=\"ifRoom\" " + - "instead", - - "Using `showAsAction=\"always\"` in menu XML, or `MenuItem.SHOW_AS_ACTION_ALWAYS` in " + - "Java code is usually a deviation from the user interface style guide." + - "Use `ifRoom` or the corresponding `MenuItem.SHOW_AS_ACTION_IF_ROOM` instead.\n" + - "\n" + - "If `always` is used sparingly there are usually no problems and behavior is " + - "roughly equivalent to `ifRoom` but with preference over other `ifRoom` " + - "items. Using it more than twice in the same menu is a bad idea.\n" + - "\n" + - "This check looks for menu XML files that contain more than two `always` " + - "actions, or some `always` actions and no `ifRoom` actions. In Java code, " + - "it looks for projects that contain references to `MenuItem.SHOW_AS_ACTION_ALWAYS` " + - "and no references to `MenuItem.SHOW_AS_ACTION_IF_ROOM`.", - - Category.USABILITY, - 3, - Severity.WARNING, - AlwaysShowActionDetector.class, - EnumSet.of(Scope.RESOURCE_FILE, Scope.JAVA_FILE)).setMoreInfo( - "http://developer.android.com/design/patterns/actionbar.html"); //$NON-NLS-1$ - - /** List of showAsAction attributes appearing in the current menu XML file */ - private List<Attr> mFileAttributes; - /** If at least one location has been marked ignore in this file, ignore all */ - private boolean mIgnoreFile; - /** List of locations of MenuItem.SHOW_AS_ACTION_ALWAYS references in Java code */ - private List<Location> mAlwaysFields; - /** True if references to MenuItem.SHOW_AS_ACTION_IF_ROOM were found */ - private boolean mHasIfRoomRefs; - - /** Constructs a new {@link AlwaysShowActionDetector} */ - public AlwaysShowActionDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.MENU; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_SHOW_AS_ACTION); - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - mFileAttributes = null; - } - - @Override - public void afterCheckFile(@NonNull Context context) { - if (mIgnoreFile) { - mFileAttributes = null; - return; - } - if (mFileAttributes != null) { - assert context instanceof XmlContext; // mFileAttributes is only set in XML files - - List<Attr> always = new ArrayList<Attr>(); - List<Attr> ifRoom = new ArrayList<Attr>(); - for (Attr attribute : mFileAttributes) { - String value = attribute.getValue(); - if (value.equals(VALUE_ALWAYS)) { - always.add(attribute); - } else if (value.equals(VALUE_IF_ROOM)) { - ifRoom.add(attribute); - } else if (value.indexOf('|') != -1) { - String[] flags = value.split("\\|"); //$NON-NLS-1$ - for (String flag : flags) { - if (flag.equals(VALUE_ALWAYS)) { - always.add(attribute); - break; - } else if (flag.equals(VALUE_IF_ROOM)) { - ifRoom.add(attribute); - break; - } - } - } - } - - if (!always.isEmpty() && mFileAttributes.size() > 1) { - // Complain if you're using more than one "always", or if you're - // using "always" and aren't using "ifRoom" at all (and provided you - // have more than a single item) - if (always.size() > 2 || ifRoom.isEmpty()) { - XmlContext xmlContext = (XmlContext) context; - Location location = null; - for (int i = always.size() - 1; i >= 0; i--) { - Location next = location; - location = xmlContext.getLocation(always.get(i)); - if (next != null) { - location.setSecondary(next); - } - } - context.report(ISSUE, location, - "Prefer \"ifRoom\" instead of \"always\"", null); - } - } - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mAlwaysFields != null && !mHasIfRoomRefs) { - for (Location location : mAlwaysFields) { - context.report(ISSUE, location, - "Prefer \"SHOW_AS_ACTION_IF_ROOM\" instead of \"SHOW_AS_ACTION_ALWAYS\"", - null); - } - } - } - - // ---- Implements XmlScanner ---- - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (context.getDriver().isSuppressed(ISSUE, attribute)) { - mIgnoreFile = true; - return; - } - - if (mFileAttributes == null) { - mFileAttributes = new ArrayList<Attr>(); - } - mFileAttributes.add(attribute); - } - - // ---- Implements JavaScanner ---- - - @Override - public - List<Class<? extends Node>> getApplicableNodeTypes() { - return Collections.<Class<? extends Node>>singletonList(Select.class); - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new FieldAccessChecker(context); - } - - private class FieldAccessChecker extends ForwardingAstVisitor { - private final JavaContext mContext; - - public FieldAccessChecker(JavaContext context) { - mContext = context; - } - - @Override - public boolean visitSelect(Select node) { - String description = node.astIdentifier().getDescription(); - boolean isIfRoom = description.equals("SHOW_AS_ACTION_IF_ROOM"); //$NON-NLS-1$ - boolean isAlways = description.equals("SHOW_AS_ACTION_ALWAYS"); //$NON-NLS-1$ - if ((isIfRoom || isAlways) - && node.astOperand().toString().equals("MenuItem")) { //$NON-NLS-1$ - if (isAlways) { - if (mContext.getDriver().isSuppressed(ISSUE, node)) { - return super.visitSelect(node); - } - if (mAlwaysFields == null) { - mAlwaysFields = new ArrayList<Location>(); - } - mAlwaysFields.add(mContext.getLocation(node)); - } else { - mHasIfRoomRefs = true; - } - } - - return super.visitSelect(node); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java deleted file mode 100644 index eca9256..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/AnnotationDetector.java +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Copyright (C) 2012 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.FQCN_SUPPRESS_LINT; -import static com.android.SdkConstants.SUPPRESS_LINT; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.IssueRegistry; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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 java.io.File; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; - -import lombok.ast.Annotation; -import lombok.ast.AnnotationElement; -import lombok.ast.AnnotationValue; -import lombok.ast.ArrayInitializer; -import lombok.ast.AstVisitor; -import lombok.ast.Block; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.MethodDeclaration; -import lombok.ast.Modifiers; -import lombok.ast.Node; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.StringLiteral; -import lombok.ast.TypeBody; -import lombok.ast.VariableDefinition; -import lombok.ast.VariableDefinitionEntry; - -/** - * Checks annotations to make sure they are valid - */ -public class AnnotationDetector extends Detector implements Detector.JavaScanner { - /** Placing SuppressLint on a local variable doesn't work for class-file based checks */ - public static final Issue ISSUE = Issue.create( - "LocalSuppress", //$NON-NLS-1$ - "Looks for @SuppressLint annotations in locations where it doesn't work for class based checks", - - "The `@SuppressAnnotation` is used to suppress Lint warnings in Java files. However, " + - "while many lint checks analyzes the Java source code, where they can find " + - "annotations on (for example) local variables, some checks are analyzing the " + - "`.class` files. And in class files, annotations only appear on classes, fields " + - "and methods. Annotations placed on local variables disappear. If you attempt " + - "to suppress a lint error for a class-file based lint check, the suppress " + - "annotation not work. You must move the annotation out to the surrounding method.", - - Category.CORRECTNESS, - 3, - Severity.ERROR, - AnnotationDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Constructs a new {@link AnnotationDetector} check */ - public AnnotationDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements JavaScanner ---- - - @Override - public List<Class<? extends Node>> getApplicableNodeTypes() { - return Collections.<Class<? extends Node>>singletonList(Annotation.class); - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new AnnotationChecker(context); - } - - private static class AnnotationChecker extends ForwardingAstVisitor { - private final JavaContext mContext; - - public AnnotationChecker(JavaContext context) { - mContext = context; - } - - @Override - public boolean visitAnnotation(Annotation node) { - String type = node.astAnnotationTypeReference().getTypeName(); - if (SUPPRESS_LINT.equals(type) || FQCN_SUPPRESS_LINT.equals(type)) { - Node parent = node.getParent(); - if (parent instanceof Modifiers) { - parent = parent.getParent(); - if (parent instanceof VariableDefinition) { - for (AnnotationElement element : node.astElements()) { - AnnotationValue valueNode = element.astValue(); - if (valueNode == null) { - continue; - } - if (valueNode instanceof StringLiteral) { - StringLiteral literal = (StringLiteral) valueNode; - String id = literal.astValue(); - if (!checkId(node, id)) { - return super.visitAnnotation(node); - } - } else if (valueNode instanceof ArrayInitializer) { - ArrayInitializer array = (ArrayInitializer) valueNode; - StrictListAccessor<Expression, ArrayInitializer> expressions = - array.astExpressions(); - if (expressions == null) { - continue; - } - Iterator<Expression> arrayIterator = expressions.iterator(); - while (arrayIterator.hasNext()) { - Expression arrayElement = arrayIterator.next(); - if (arrayElement instanceof StringLiteral) { - String id = ((StringLiteral) arrayElement).astValue(); - if (!checkId(node, id)) { - return super.visitAnnotation(node); - } - } - } - } - } - } - } - } - - return super.visitAnnotation(node); - } - - private boolean checkId(Annotation node, String id) { - IssueRegistry registry = mContext.getDriver().getRegistry(); - Issue issue = registry.getIssue(id); - // Special-case the ApiDetector issue, since it does both source file analysis - // only on field references, and class file analysis on the rest, so we allow - // annotations outside of methods only on fields - if (issue != null && !issue.getScope().contains(Scope.JAVA_FILE) - || issue == ApiDetector.UNSUPPORTED) { - // Ensure that this isn't a field - Node parent = node.getParent(); - while (parent != null) { - if (parent instanceof MethodDeclaration - || parent instanceof ConstructorDeclaration - || parent instanceof Block) { - break; - } else if (parent instanceof TypeBody) { // It's a field - return true; - } else if (issue == ApiDetector.UNSUPPORTED - && parent instanceof VariableDefinition) { - VariableDefinition definition = (VariableDefinition) parent; - for (VariableDefinitionEntry entry : definition.astVariables()) { - Expression initializer = entry.astInitializer(); - if (initializer instanceof Select) { - return true; - } - } - } - parent = parent.getParent(); - if (parent == null) { - return true; - } - } - - // This issue doesn't have AST access: annotations are not - // available for local variables or parameters - mContext.report(ISSUE, node, mContext.getLocation(node), String.format( - "The @SuppressLint annotation cannot be used on a local " + - "variable with the lint check '%1$s': move out to the " + - "surrounding method", id), - null); - return false; - } - - return true; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Api.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Api.java deleted file mode 100644 index ca84b27..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Api.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2012 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 org.xml.sax.SAXException; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -/** - * Main entry point for API description. - * - * To create the {@link Api}, use {@link #parseApi(File)} - * - */ -public class Api { - - /** - * Parses simplified API file. - * @param apiFile the file to read - * @return a new ApiInfo - */ - public static Api parseApi(File apiFile) { - FileInputStream fileInputStream = null; - try { - fileInputStream = new FileInputStream(apiFile); - SAXParserFactory parserFactory = SAXParserFactory.newInstance(); - SAXParser parser = parserFactory.newSAXParser(); - ApiParser apiParser = new ApiParser(); - parser.parse(fileInputStream, apiParser); - return new Api(apiParser.getClasses()); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - } catch (SAXException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (fileInputStream != null) { - try { - fileInputStream.close(); - } catch (IOException e) { - // ignore - } - } - } - - return null; - } - - private final Map<String, ApiClass> mClasses; - - private Api(Map<String, ApiClass> classes) { - mClasses = new HashMap<String, ApiClass>(classes); - } - - ApiClass getClass(String fqcn) { - return mClasses.get(fqcn); - } - - Map<String, ApiClass> getClasses() { - return Collections.unmodifiableMap(mClasses); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiClass.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiClass.java deleted file mode 100644 index 3e0fb9d..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiClass.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * Copyright (C) 2012 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.CONSTRUCTOR_NAME; - -import com.android.annotations.Nullable; -import com.android.utils.Pair; -import com.google.common.collect.Lists; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Represents a class and its methods/fields. - * - * {@link #getSince()} gives the API level it was introduced. - * - * {@link #getMethod} returns when the method was introduced. - * {@link #getField} returns when the field was introduced. - */ -public class ApiClass { - - private final String mName; - private final int mSince; - - private final List<Pair<String, Integer>> mSuperClasses = Lists.newArrayList(); - private final List<Pair<String, Integer>> mInterfaces = Lists.newArrayList(); - - private final Map<String, Integer> mFields = new HashMap<String, Integer>(); - private final Map<String, Integer> mMethods = new HashMap<String, Integer>(); - - ApiClass(String name, int since) { - mName = name; - mSince = since; - } - - /** - * Returns the name of the class. - * @return the name of the class - */ - String getName() { - return mName; - } - - /** - * Returns when the class was introduced. - * @return the api level the class was introduced. - */ - int getSince() { - return mSince; - } - - /** - * Returns when a field was added, or null if it doesn't exist. - * @param name the name of the field. - * @param info the corresponding info - */ - Integer getField(String name, Api info) { - // The field can come from this class or from a super class or an interface - // The value can never be lower than this introduction of this class. - // When looking at super classes and interfaces, it can never be lower than when the - // super class or interface was added as a super class or interface to this class. - // Look at all the values and take the lowest. - // For instance: - // This class A is introduced in 5 with super class B. - // In 10, the interface C was added. - // Looking for SOME_FIELD we get the following: - // Present in A in API 15 - // Present in B in API 11 - // Present in C in API 7. - // The answer is 10, which is when C became an interface - int min = Integer.MAX_VALUE; - Integer i = mFields.get(name); - if (i != null) { - min = i; - } - - // now look at the super classes - for (Pair<String, Integer> superClassPair : mSuperClasses) { - ApiClass superClass = info.getClass(superClassPair.getFirst()); - if (superClass != null) { - i = superClass.getField(name, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } - } - } - } - - // now look at the interfaces - for (Pair<String, Integer> superClassPair : mInterfaces) { - ApiClass superClass = info.getClass(superClassPair.getFirst()); - if (superClass != null) { - i = superClass.getField(name, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } - } - } - } - - return min; - } - - /** - * Returns when a method was added, or null if it doesn't exist. This goes through the super - * class to find method only present there. - * @param methodSignature the method signature - */ - int getMethod(String methodSignature, Api info) { - // The method can come from this class or from a super class. - // The value can never be lower than this introduction of this class. - // When looking at super classes, it can never be lower than when the super class became - // a super class of this class. - // Look at all the values and take the lowest. - // For instance: - // This class A is introduced in 5 with super class B. - // In 10, the super class changes to C. - // Looking for foo() we get the following: - // Present in A in API 15 - // Present in B in API 11 - // Present in C in API 7. - // The answer is 10, which is when C became the super class. - int min = Integer.MAX_VALUE; - Integer i = mMethods.get(methodSignature); - if (i != null) { - min = i; - - // Constructors aren't inherited - if (methodSignature.startsWith(CONSTRUCTOR_NAME)) { - return i; - } - } - - // now look at the super classes - for (Pair<String, Integer> superClassPair : mSuperClasses) { - ApiClass superClass = info.getClass(superClassPair.getFirst()); - if (superClass != null) { - i = superClass.getMethod(methodSignature, info); - if (i != null) { - int tmp = superClassPair.getSecond() > i ? superClassPair.getSecond() : i; - if (tmp < min) { - min = tmp; - } - } - } - } - - // now look at the interfaces classes - for (Pair<String, Integer> interfacePair : mInterfaces) { - ApiClass superClass = info.getClass(interfacePair.getFirst()); - if (superClass != null) { - i = superClass.getMethod(methodSignature, info); - if (i != null) { - int tmp = interfacePair.getSecond() > i ? interfacePair.getSecond() : i; - if (tmp < min) { - min = tmp; - } - } - } - } - - return min; - } - - void addField(String name, int since) { - Integer i = mFields.get(name); - if (i == null || i.intValue() > since) { - mFields.put(name, Integer.valueOf(since)); - } - } - - void addMethod(String name, int since) { - // Strip off the method type at the end to ensure that the code which - // produces inherited methods doesn't get confused and end up multiple entries. - // For example, java/nio/Buffer has the method "array()Ljava/lang/Object;", - // and the subclass java/nio/ByteBuffer has the method "array()[B". We want - // the lookup on mMethods to associate the ByteBuffer array method to be - // considered overriding the Buffer method. - int index = name.indexOf(')'); - if (index != -1) { - name = name.substring(0, index + 1); - } - - Integer i = mMethods.get(name); - if (i == null || i.intValue() > since) { - mMethods.put(name, Integer.valueOf(since)); - } - } - - void addSuperClass(String superClass, int since) { - addToArray(mSuperClasses, superClass, since); - } - - void addInterface(String interfaceClass, int since) { - addToArray(mInterfaces, interfaceClass, since); - } - - void addToArray(List<Pair<String, Integer>> list, String name, int value) { - // check if we already have that name (at a lower level) - for (Pair<String, Integer> pair : list) { - if (name.equals(pair.getFirst())) { - return; - } - } - - list.add(Pair.of(name, Integer.valueOf(value))); - - } - - @Nullable - public String getPackage() { - int index = mName.lastIndexOf('/'); - if (index != -1) { - return mName.substring(0, index); - } - - return null; - } - - @Override - public String toString() { - return mName; - } - - /** - * Returns the set of all methods, including inherited - * ones. - * - * @param info the api to look up super classes from - * @return a set containing all the members fields - */ - Set<String> getAllMethods(Api info) { - Set<String> members = new HashSet<String>(100); - addAllMethods(info, members, true /*includeConstructors*/); - - return members; - } - - private void addAllMethods(Api info, Set<String> set, boolean includeConstructors) { - if (!includeConstructors) { - for (String method : mMethods.keySet()) { - if (!method.startsWith(CONSTRUCTOR_NAME)) { - set.add(method); - } - } - } else { - for (String method : mMethods.keySet()) { - set.add(method); - } - } - - for (Pair<String, Integer> superClass : mSuperClasses) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllMethods(info, set, false); - } - } - - // Get methods from implemented interfaces as well; - for (Pair<String, Integer> superClass : mInterfaces) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllMethods(info, set, false); - } - } - } - - /** - * Returns the set of all fields, including inherited - * ones. - * - * @param info the api to look up super classes from - * @return a set containing all the fields - */ - Set<String> getAllFields(Api info) { - Set<String> members = new HashSet<String>(100); - addAllFields(info, members); - - return members; - } - - private void addAllFields(Api info, Set<String> set) { - for (String field : mFields.keySet()) { - set.add(field); - } - - for (Pair<String, Integer> superClass : mSuperClasses) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllFields(info, set); - } - } - - // Get methods from implemented interfaces as well; - for (Pair<String, Integer> superClass : mInterfaces) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - clz.addAllFields(info, set); - } - } - } - - /* This code can be used to scan through all the fields and look for fields - that have moved to a higher class: - Field android/view/MotionEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9 - Field android/provider/ContactsContract$CommonDataKinds$Organization#PHONETIC_NAME has api=5 but parent android/provider/ContactsContract$ContactNameColumns provides it as 11 - Field android/widget/ListView#CHOICE_MODE_MULTIPLE has api=1 but parent android/widget/AbsListView provides it as 11 - Field android/widget/ListView#CHOICE_MODE_NONE has api=1 but parent android/widget/AbsListView provides it as 11 - Field android/widget/ListView#CHOICE_MODE_SINGLE has api=1 but parent android/widget/AbsListView provides it as 11 - Field android/view/KeyEvent#CREATOR has api=1 but parent android/view/InputEvent provides it as 9 - This is used for example in the ApiDetector to filter out warnings which result - when people follow Eclipse's advice to replace - ListView.CHOICE_MODE_MULTIPLE - references with - AbsListView.CHOICE_MODE_MULTIPLE - since the latter has API=11 and the former has API=1; since the constant is unchanged - between the two, and the literal is copied into the class, using the AbsListView - reference works. - public void checkFields(Api info) { - fieldLoop: - for (String field : mFields.keySet()) { - Integer since = getField(field, info); - if (since == null || since == Integer.MAX_VALUE) { - continue; - } - - for (Pair<String, Integer> superClass : mSuperClasses) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - Integer superSince = clz.getField(field, info); - if (superSince == Integer.MAX_VALUE) { - continue; - } - - if (superSince != null && superSince > since) { - String declaredIn = clz.findFieldDeclaration(info, field); - System.out.println("Field " + getName() + "#" + field + " has api=" - + since + " but parent " + declaredIn + " provides it as " - + superSince); - continue fieldLoop; - } - } - } - - // Get methods from implemented interfaces as well; - for (Pair<String, Integer> superClass : mInterfaces) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - Integer superSince = clz.getField(field, info); - if (superSince == Integer.MAX_VALUE) { - continue; - } - if (superSince != null && superSince > since) { - String declaredIn = clz.findFieldDeclaration(info, field); - System.out.println("Field " + getName() + "#" + field + " has api=" - + since + " but parent " + declaredIn + " provides it as " - + superSince); - continue fieldLoop; - } - } - } - } - } - - private String findFieldDeclaration(Api info, String name) { - if (mFields.containsKey(name)) { - return getName(); - } - for (Pair<String, Integer> superClass : mSuperClasses) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - String declaredIn = clz.findFieldDeclaration(info, name); - if (declaredIn != null) { - return declaredIn; - } - } - } - - // Get methods from implemented interfaces as well; - for (Pair<String, Integer> superClass : mInterfaces) { - ApiClass clz = info.getClass(superClass.getFirst()); - assert clz != null : superClass.getSecond(); - if (clz != null) { - String declaredIn = clz.findFieldDeclaration(info, name); - if (declaredIn != null) { - return declaredIn; - } - } - } - - return null; - } - */ -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java deleted file mode 100644 index cc2b212..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiDetector.java +++ /dev/null @@ -1,1569 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_PREFIX; -import static com.android.SdkConstants.ANDROID_THEME_PREFIX; -import static com.android.SdkConstants.ATTR_CLASS; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_TARGET_API; -import static com.android.SdkConstants.CONSTRUCTOR_NAME; -import static com.android.SdkConstants.PREFIX_ANDROID; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.SdkConstants.TARGET_API; -import static com.android.SdkConstants.TOOLS_URI; -import static com.android.SdkConstants.VIEW_TAG; -import static com.android.tools.lint.detector.api.ClassContext.getFqcn; -import static com.android.tools.lint.detector.api.ClassContext.getInternalName; -import static com.android.tools.lint.detector.api.LintUtils.getNextInstruction; -import static com.android.tools.lint.detector.api.Location.SearchDirection.BACKWARD; -import static com.android.tools.lint.detector.api.Location.SearchDirection.FORWARD; -import static com.android.tools.lint.detector.api.Location.SearchDirection.NEAREST; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.DefaultPosition; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.SearchHints; -import com.android.tools.lint.detector.api.Position; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.android.utils.Pair; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.AnnotationNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.IntInsnNode; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.LocalVariableNode; -import org.objectweb.asm.tree.LookupSwitchInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import lombok.ast.Annotation; -import lombok.ast.AnnotationElement; -import lombok.ast.AnnotationValue; -import lombok.ast.AstVisitor; -import lombok.ast.BinaryExpression; -import lombok.ast.Case; -import lombok.ast.ClassDeclaration; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.ConstructorInvocation; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.If; -import lombok.ast.ImportDeclaration; -import lombok.ast.InlineIfExpression; -import lombok.ast.IntegralLiteral; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.Modifiers; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.StringLiteral; -import lombok.ast.SuperConstructorInvocation; -import lombok.ast.Switch; -import lombok.ast.TypeReference; -import lombok.ast.VariableDefinition; -import lombok.ast.VariableDefinitionEntry; -import lombok.ast.VariableReference; - -/** - * Looks for usages of APIs that are not supported in all the versions targeted - * by this application (according to its minimum API requirement in the manifest). - */ -public class ApiDetector extends ResourceXmlDetector - implements Detector.ClassScanner, Detector.JavaScanner { - /** - * Whether we flag variable, field, parameter and return type declarations of a type - * not yet available. It appears Dalvik is very forgiving and doesn't try to preload - * classes until actually needed, so there is no need to flag these, and in fact, - * patterns used for supporting new and old versions sometimes declares these methods - * and only conditionally end up actually accessing methods and fields, so only check - * method and field accesses. - */ - private static final boolean CHECK_DECLARATIONS = false; - - private static final boolean AOSP_BUILD = System.getenv("ANDROID_BUILD_TOP") != null; //$NON-NLS-1$ - - /** Accessing an unsupported API */ - public static final Issue UNSUPPORTED = Issue.create("NewApi", //$NON-NLS-1$ - "Finds API accesses to APIs that are not supported in all targeted API versions", - - "This check scans through all the Android API calls in the application and " + - "warns about any calls that are not available on *all* versions targeted " + - "by this application (according to its minimum SDK attribute in the manifest).\n" - + - "\n" + - "If you really want to use this API and don't need to support older devices just " - + - "set the `minSdkVersion` in your `AndroidManifest.xml` file." + - "\n" + - "If your code is *deliberately* accessing newer APIs, and you have ensured " + - "(e.g. with conditional execution) that this code will only ever be called on a " - + - "supported platform, then you can annotate your class or method with the " + - "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + - "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " - + - "file's minimum SDK as the required API level.\n" + - "\n" + - "If you are deliberately setting `android:` attributes in style definitions, " + - "make sure you place this in a `values-v11` folder in order to avoid running " + - "into runtime conflicts on certain devices where manufacturers have added " + - "custom attributes whose ids conflict with the new ones on later platforms.\n" + - "\n" + - "Similarly, you can use tools:targetApi=\"11\" in an XML file to indicate that " - + - "the element will only be inflated in an adequate context.", - Category.CORRECTNESS, - 6, - Severity.ERROR, - ApiDetector.class, - EnumSet.of(Scope.CLASS_FILE, Scope.RESOURCE_FILE, Scope.MANIFEST)) - .addAnalysisScope(Scope.RESOURCE_FILE_SCOPE) - .addAnalysisScope(Scope.CLASS_FILE_SCOPE); - - /** Accessing an inlined API on older platforms */ - public static final Issue INLINED = Issue.create("InlinedApi", //$NON-NLS-1$ - "Finds inlined fields that may or may not work on older platforms", - - "This check scans through all the Android API field references in the application " + - "and flags certain constants, such as static final integers and Strings, " + - "which were introduced in later versions. These will actually be copied " + - "into the class files rather than being referenced, which means that " + - "the value is available even when running on older devices. In some " + - "cases that's fine, and in other cases it can result in a runtime " + - "crash or incorrect behavior. It depends on the context, so consider " + - "the code carefully and device whether it's safe and can be suppressed " + - "or whether the code needs tbe guarded.\n" + - "\n" + - "If you really want to use this API and don't need to support older devices just " - + - "set the `minSdkVersion` in your `AndroidManifest.xml` file." + - "\n" + - "If your code is *deliberately* accessing newer APIs, and you have ensured " + - "(e.g. with conditional execution) that this code will only ever be called on a " - + - "supported platform, then you can annotate your class or method with the " + - "`@TargetApi` annotation specifying the local minimum SDK to apply, such as " + - "`@TargetApi(11)`, such that this check considers 11 rather than your manifest " - + - "file's minimum SDK as the required API level.\n", - Category.CORRECTNESS, - 6, - Severity.WARNING, - ApiDetector.class, - EnumSet.of(Scope.JAVA_FILE)) - .addAnalysisScope(Scope.JAVA_FILE_SCOPE); - - /** Accessing an unsupported API */ - public static final Issue OVERRIDE = Issue.create("Override", //$NON-NLS-1$ - "Finds method declarations that will accidentally override methods in later versions", - - "Suppose you are building against Android API 8, and you've subclassed Activity. " + - "In your subclass you add a new method called `isDestroyed`(). At some later point, " + - "a method of the same name and signature is added to Android. Your method will " + - "now override the Android method, and possibly break its contract. Your method " + - "is not calling `super.isDestroyed()`, since your compilation target doesn't " + - "know about the method.\n" + - "\n" + - "The above scenario is what this lint detector looks for. The above example is " + - "real, since `isDestroyed()` was added in API 17, but it will be true for *any* " + - "method you have added to a subclass of an Android class where your build target " + - "is lower than the version the method was introduced in.\n" + - "\n" + - "To fix this, either rename your method, or if you are really trying to augment " + - "the builtin method if available, switch to a higher build target where you can " + - "deliberately add `@Override` on your overriding method, and call `super` if " + - "appropriate etc.\n", - Category.CORRECTNESS, - 6, - Severity.ERROR, - ApiDetector.class, - Scope.CLASS_FILE_SCOPE); - - private static final String TARGET_API_VMSIG = '/' + TARGET_API + ';'; - private static final String SWITCH_TABLE_PREFIX = "$SWITCH_TABLE$"; //$NON-NLS-1$ - private static final String ORDINAL_METHOD = "ordinal"; //$NON-NLS-1$ - - protected ApiLookup mApiDatabase; - private int mMinApi = -1; - private Map<String, List<Pair<String, Location>>> mPendingFields; - - /** Constructs a new API check */ - public ApiDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - mApiDatabase = ApiLookup.get(context.getClient()); - // We can't look up the minimum API required by the project here: - // The manifest file hasn't been processed yet in the -before- project hook. - // For now it's initialized lazily in getMinSdk(Context), but the - // lint infrastructure should be fixed to parse manifest file up front. - } - - // ---- Implements XmlScanner ---- - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return true; - } - - @Override - public Collection<String> getApplicableElements() { - return ALL; - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (mApiDatabase == null) { - return; - } - - String value = attribute.getValue(); - - String owner = null; - String name = null; - - String prefix; - if (value.startsWith(ANDROID_PREFIX)) { - prefix = ANDROID_PREFIX; - } else if (value.startsWith(ANDROID_THEME_PREFIX)) { - prefix = ANDROID_THEME_PREFIX; - } else if (value.startsWith(PREFIX_ANDROID) && ATTR_NAME.equals(attribute.getName()) - && TAG_ITEM.equals(attribute.getOwnerElement().getTagName()) - && attribute.getOwnerElement().getParentNode() != null - && TAG_STYLE.equals(attribute.getOwnerElement().getParentNode().getNodeName())) { - owner = "android/R$attr"; //$NON-NLS-1$ - name = value.substring(PREFIX_ANDROID.length()); - prefix = PREFIX_ANDROID; - } else { - return; - } - - if (owner == null) { - // Convert @android:type/foo into android/R$type and "foo" - int index = value.indexOf('/', prefix.length()); - if (index != -1) { - owner = "android/R$" //$NON-NLS-1$ - + value.substring(prefix.length(), index); - name = value.substring(index + 1); - if (name.indexOf('.') != -1) { - name = name.replace('.', '_'); - } - } else if (value.startsWith(ANDROID_THEME_PREFIX)) { - owner = "android/R$attr"; //$NON-NLS-1$ - name = value.substring(ANDROID_THEME_PREFIX.length()); - } else { - return; - } - } - assert name != null; // Eclipse can't infer this - int api = mApiDatabase.getFieldVersion(owner, name); - int minSdk = getMinSdk(context); - if (api > minSdk && api > context.getFolderVersion() - && api > getLocalMinSdk(attribute.getOwnerElement())) { - // Don't complain about resource references in the tools namespace, - // such as for example "tools:layout="@android:layout/list_content", - // used only for designtime previews - if (TOOLS_URI.equals(attribute.getNamespaceURI())) { - return; - } - - Location location = context.getLocation(attribute); - String message = String.format( - "%1$s requires API level %2$d (current min is %3$d)", - value, api, minSdk); - context.report(UNSUPPORTED, attribute, location, message, null); - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (mApiDatabase == null) { - return; - } - - String tag = element.getTagName(); - - ResourceFolderType folderType = context.getResourceFolderType(); - if (folderType != ResourceFolderType.LAYOUT) { - if (element.getParentNode().getNodeType() != Node.ELEMENT_NODE) { - // Root node - return; - } - NodeList childNodes = element.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node textNode = childNodes.item(i); - if (textNode.getNodeType() == Node.TEXT_NODE) { - String text = textNode.getNodeValue(); - if (text.indexOf(ANDROID_PREFIX) != -1) { - text = text.trim(); - // Convert @android:type/foo into android/R$type and "foo" - int index = text.indexOf('/', ANDROID_PREFIX.length()); - if (index != -1) { - String owner = "android/R$" //$NON-NLS-1$ - + text.substring(ANDROID_PREFIX.length(), index); - String name = text.substring(index + 1); - if (name.indexOf('.') != -1) { - name = name.replace('.', '_'); - } - int api = mApiDatabase.getFieldVersion(owner, name); - int minSdk = getMinSdk(context); - if (api > minSdk && api > context.getFolderVersion() - && api > getLocalMinSdk(element)) { - Location location = context.getLocation(textNode); - String message = String.format( - "%1$s requires API level %2$d (current min is %3$d)", - text, api, minSdk); - context.report(UNSUPPORTED, element, location, message, null); - } - } - } - } - } - } else if (folderType == ResourceFolderType.LAYOUT) { - if (VIEW_TAG.equals(tag)) { - tag = element.getAttribute(ATTR_CLASS); - if (tag == null || tag.isEmpty()) { - return; - } - } - - // Check widgets to make sure they're available in this version of the SDK. - if (tag.indexOf('.') != -1 || - folderType != ResourceFolderType.LAYOUT) { - // Custom views aren't in the index - return; - } - // TODO: Consider other widgets outside of android.widget.* - int api = mApiDatabase.getCallVersion("android/widget/" + tag, //$NON-NLS-1$ - CONSTRUCTOR_NAME, - // Not all views provided this constructor right away, for example, - // LinearLayout added it in API 11 yet LinearLayout is much older: - // "(Landroid/content/Context;Landroid/util/AttributeSet;I)V"); //$NON-NLS-1$ - "(Landroid/content/Context;)"); //$NON-NLS-1$ - int minSdk = getMinSdk(context); - if (api > minSdk && api > context.getFolderVersion() - && api > getLocalMinSdk(element)) { - Location location = context.getLocation(element); - String message = String.format( - "View requires API level %1$d (current min is %2$d): <%3$s>", - api, minSdk, tag); - context.report(UNSUPPORTED, element, location, message, null); - } - } - } - - protected int getMinSdk(Context context) { - if (mMinApi == -1) { - mMinApi = context.getMainProject().getMinSdk(); - } - - return mMinApi; - } - - // ---- Implements ClassScanner ---- - - @SuppressWarnings("rawtypes") // ASM API - @Override - public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) { - if (mApiDatabase == null) { - return; - } - - if (AOSP_BUILD && classNode.name.startsWith("android/support/")) { //$NON-NLS-1$ - return; - } - - // Requires util package (add prebuilts/tools/common/asm-tools/asm-debug-all-4.0.jar) - //classNode.accept(new TraceClassVisitor(new PrintWriter(System.out))); - - int classMinSdk = getClassMinSdk(context, classNode); - if (classMinSdk == -1) { - classMinSdk = getMinSdk(context); - } - - List methodList = classNode.methods; - if (methodList.isEmpty()) { - return; - } - - boolean checkCalls = context.isEnabled(UNSUPPORTED) - || context.isEnabled(INLINED); - boolean checkMethods = context.isEnabled(OVERRIDE) - && context.getMainProject().getBuildSdk() >= 1; - String frameworkParent = null; - if (checkMethods) { - LintDriver driver = context.getDriver(); - String owner = classNode.superName; - while (owner != null) { - // For virtual dispatch, walk up the inheritance chain checking - // each inherited method - if ((owner.startsWith("android/") //$NON-NLS-1$ - && !owner.startsWith("android/support/")) //$NON-NLS-1$ - || owner.startsWith("java/") //$NON-NLS-1$ - || owner.startsWith("javax/")) { //$NON-NLS-1$ - frameworkParent = owner; - break; - } - owner = driver.getSuperClass(owner); - } - if (frameworkParent == null) { - checkMethods = false; - } - } - - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - - int minSdk = getLocalMinSdk(method.invisibleAnnotations); - if (minSdk == -1) { - minSdk = classMinSdk; - } - - InsnList nodes = method.instructions; - - if (checkMethods && Character.isJavaIdentifierStart(method.name.charAt(0))) { - int buildSdk = context.getMainProject().getBuildSdk(); - String name = method.name; - assert frameworkParent != null; - int api = mApiDatabase.getCallVersion(frameworkParent, name, method.desc); - if (api > buildSdk && buildSdk != -1) { - // TODO: Don't complain if it's annotated with @Override; that means - // somehow the build target isn't correct. - String fqcn; - String owner = classNode.name; - if (CONSTRUCTOR_NAME.equals(name)) { - fqcn = "new " + ClassContext.getFqcn(owner); //$NON-NLS-1$ - } else { - fqcn = ClassContext.getFqcn(owner) + '#' + name; - } - String message = String.format( - "This method is not overriding anything with the current build " + - "target, but will in API level %1$d (current target is %2$d): %3$s", - api, buildSdk, fqcn); - - Location location = context.getLocation(method, classNode); - context.report(OVERRIDE, method, null, location, message, null); - } - } - - if (!checkCalls) { - continue; - } - - if (CHECK_DECLARATIONS) { - // Check types in parameter list and types of local variables - List localVariables = method.localVariables; - if (localVariables != null) { - for (Object v : localVariables) { - LocalVariableNode var = (LocalVariableNode) v; - String desc = var.desc; - if (desc.charAt(0) == 'L') { - // "Lpackage/Class;" => "package/Bar" - String className = desc.substring(1, desc.length() - 1); - int api = mApiDatabase.getClassVersion(className); - if (api > minSdk) { - String fqcn = ClassContext.getFqcn(className); - String message = String.format( - "Class requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - report(context, message, var.start, method, - className.substring(className.lastIndexOf('/') + 1), null, - SearchHints.create(NEAREST).matchJavaSymbol()); - } - } - } - } - - // Check return type - // The parameter types are already handled as local variables so we can skip - // right to the return type. - // Check types in parameter list - String signature = method.desc; - if (signature != null) { - int args = signature.indexOf(')'); - if (args != -1 && signature.charAt(args + 1) == 'L') { - String type = signature.substring(args + 2, signature.length() - 1); - int api = mApiDatabase.getClassVersion(type); - if (api > minSdk) { - String fqcn = ClassContext.getFqcn(type); - String message = String.format( - "Class requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null; - report(context, message, first, method, method.name, null, - SearchHints.create(BACKWARD).matchJavaSymbol()); - } - } - } - } - - for (int i = 0, n = nodes.size(); i < n; i++) { - AbstractInsnNode instruction = nodes.get(i); - int type = instruction.getType(); - if (type == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode node = (MethodInsnNode) instruction; - String name = node.name; - String owner = node.owner; - String desc = node.desc; - - // No need to check methods in this local class; we know they - // won't be an API match - if (node.getOpcode() == Opcodes.INVOKEVIRTUAL - && owner.equals(classNode.name)) { - owner = classNode.superName; - } - - boolean checkingSuperClass = false; - while (owner != null) { - int api = mApiDatabase.getCallVersion(owner, name, desc); - if (api > minSdk) { - if (method.name.startsWith(SWITCH_TABLE_PREFIX)) { - // We're in a compiler-generated method to generate an - // array indexed by enum ordinal values to enum values. The enum - // itself must be requiring a higher API number than is - // currently used, but the call site for the switch statement - // will also be referencing it, so no need to report these - // calls. - break; - } - - if (!checkingSuperClass - && node.getOpcode() == Opcodes.INVOKEVIRTUAL - && methodDefinedLocally(classNode, name, desc)) { - break; - } - - String fqcn; - if (CONSTRUCTOR_NAME.equals(name)) { - fqcn = "new " + ClassContext.getFqcn(owner); //$NON-NLS-1$ - } else { - fqcn = ClassContext.getFqcn(owner) + '#' + name; - } - String message = String.format( - "Call requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - - if (name.equals(ORDINAL_METHOD) - && instruction.getNext() != null - && instruction.getNext().getNext() != null - && instruction.getNext().getOpcode() == Opcodes.IALOAD - && instruction.getNext().getNext().getOpcode() - == Opcodes.TABLESWITCH) { - message = String.format( - "Enum for switch requires API level %1$d " + - "(current min is %2$d): %3$s", - api, minSdk, ClassContext.getFqcn(owner)); - } - - report(context, message, node, method, name, null, - SearchHints.create(FORWARD).matchJavaSymbol()); - } - - // For virtual dispatch, walk up the inheritance chain checking - // each inherited method - if (owner.startsWith("android/") //$NON-NLS-1$ - || owner.startsWith("javax/")) { //$NON-NLS-1$ - // The API map has already inlined all inherited methods - // so no need to keep checking up the chain - // -- unless it's the support library which is also in - // the android/ namespace: - if (owner.startsWith("android/support/")) { //$NON-NLS-1$ - owner = context.getDriver().getSuperClass(owner); - } else { - owner = null; - } - } else if (owner.startsWith("java/")) { //$NON-NLS-1$ - if (owner.equals(LocaleDetector.DATE_FORMAT_OWNER)) { - checkSimpleDateFormat(context, method, node, minSdk); - } - // Already inlined; see comment above - owner = null; - } else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { - owner = context.getDriver().getSuperClass(owner); - } else if (node.getOpcode() == Opcodes.INVOKESTATIC && api == -1) { - // Inherit through static classes as well - owner = context.getDriver().getSuperClass(owner); - } else { - owner = null; - } - - checkingSuperClass = true; - } - } else if (type == AbstractInsnNode.FIELD_INSN) { - FieldInsnNode node = (FieldInsnNode) instruction; - String name = node.name; - String owner = node.owner; - int api = mApiDatabase.getFieldVersion(owner, name); - if (api > minSdk) { - if (method.name.startsWith(SWITCH_TABLE_PREFIX)) { - checkSwitchBlock(context, classNode, node, method, name, owner, - api, minSdk); - continue; - } - String fqcn = ClassContext.getFqcn(owner) + '#' + name; - if (mPendingFields != null) { - mPendingFields.remove(fqcn); - } - String message = String.format( - "Field requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - report(context, message, node, method, name, null, - SearchHints.create(FORWARD).matchJavaSymbol()); - } - } else if (type == AbstractInsnNode.LDC_INSN) { - LdcInsnNode node = (LdcInsnNode) instruction; - if (node.cst instanceof Type) { - Type t = (Type) node.cst; - String className = t.getInternalName(); - - int api = mApiDatabase.getClassVersion(className); - if (api > minSdk) { - String fqcn = ClassContext.getFqcn(className); - String message = String.format( - "Class requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - report(context, message, node, method, - className.substring(className.lastIndexOf('/') + 1), null, - SearchHints.create(FORWARD).matchJavaSymbol()); - } - } - } - } - } - } - - private static void checkSimpleDateFormat(ClassContext context, MethodNode method, - MethodInsnNode node, int minSdk) { - if (minSdk >= 9) { - // Already OK - return; - } - if (node.name.equals(CONSTRUCTOR_NAME) && !node.desc.equals("()V")) { //$NON-NLS-1$ - // Check first argument - AbstractInsnNode prev = LintUtils.getPrevInstruction(node); - if (prev != null && !node.desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$ - prev = LintUtils.getPrevInstruction(prev); - } - if (prev != null && prev.getOpcode() == Opcodes.LDC) { - LdcInsnNode ldc = (LdcInsnNode) prev; - Object cst = ldc.cst; - if (cst instanceof String) { - String pattern = (String) cst; - boolean isEscaped = false; - for (int i = 0; i < pattern.length(); i++) { - char c = pattern.charAt(i); - if (c == '\'') { - isEscaped = !isEscaped; - } else if (!isEscaped && (c == 'L' || c == 'c')) { - String message = String.format( - "The pattern character '%1$c' requires API level 9 (current " + - "min is %2$d) : \"%3$s\"", c, minSdk, pattern); - report(context, message, node, method, pattern, null, - SearchHints.create(FORWARD)); - return; - } - } - } - } - } - } - - @SuppressWarnings("rawtypes") // ASM API - private static boolean methodDefinedLocally(ClassNode classNode, String name, String desc) { - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - if (name.equals(method.name) && desc.equals(method.desc)) { - return true; - } - } - - return false; - } - - @SuppressWarnings("rawtypes") // ASM API - private static void checkSwitchBlock(ClassContext context, ClassNode classNode, - FieldInsnNode field, MethodNode method, String name, String owner, int api, - int minSdk) { - // Switch statements on enums are tricky. The compiler will generate a method - // which returns an array of the enum constants, indexed by their ordinal() values. - // However, we only want to complain if the code is actually referencing one of - // the non-available enum fields. - // - // For the android.graphics.PorterDuff.Mode enum for example, the first few items - // in the array are populated like this: - // - // L0 - // ALOAD 0 - // GETSTATIC android/graphics/PorterDuff$Mode.ADD : Landroid/graphics/PorterDuff$Mode; - // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I - // ICONST_1 - // IASTORE - // L1 - // GOTO L3 - // L2 - // FRAME FULL [[I] [java/lang/NoSuchFieldError] - // POP - // L3 - // FRAME SAME - // ALOAD 0 - // GETSTATIC android/graphics/PorterDuff$Mode.CLEAR : Landroid/graphics/PorterDuff$Mode; - // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I - // ICONST_2 - // IASTORE - // ... - // So if we for example find that the "ADD" field isn't accessible, since it requires - // API 11, we need to - // (1) First find out what its ordinal number is. We can look at the following - // instructions to discover this; it's the "ICONST_1" and "IASTORE" instructions. - // (After ICONST_5 it moves on to BIPUSH 6, BIPUSH 7, etc.) - // (2) Find the corresponding *usage* of this switch method. For the above enum, - // the switch ordinal lookup method will be called - // "$SWITCH_TABLE$android$graphics$PorterDuff$Mode" with desc "()[I". - // This means we will be looking for an invocation in some other method which looks - // like this: - // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I - // (obviously, it can be invoked more than once) - // Note that it can be used more than once in this class and all sites should be - // checked! - // (3) Look up the corresponding table switch, which should look something like this: - // INVOKESTATIC (current class).$SWITCH_TABLE$android$graphics$PorterDuff$Mode ()[I - // ALOAD 0 - // INVOKEVIRTUAL android/graphics/PorterDuff$Mode.ordinal ()I - // IALOAD - // LOOKUPSWITCH - // 2: L1 - // 11: L2 - // default: L3 - // Here we need to see if the LOOKUPSWITCH instruction is referencing our target - // case. Above we were looking for the "ADD" case which had ordinal 1. Since this - // isn't explicitly referenced, we can ignore this field reference. - AbstractInsnNode next = field.getNext(); - if (next == null || next.getOpcode() != Opcodes.INVOKEVIRTUAL) { - return; - } - next = next.getNext(); - if (next == null) { - return; - } - int ordinal; - switch (next.getOpcode()) { - case Opcodes.ICONST_0: ordinal = 0; break; - case Opcodes.ICONST_1: ordinal = 1; break; - case Opcodes.ICONST_2: ordinal = 2; break; - case Opcodes.ICONST_3: ordinal = 3; break; - case Opcodes.ICONST_4: ordinal = 4; break; - case Opcodes.ICONST_5: ordinal = 5; break; - case Opcodes.BIPUSH: { - IntInsnNode iin = (IntInsnNode) next; - ordinal = iin.operand; - break; - } - default: - return; - } - - // Find usages of this call site - List methodList = classNode.methods; - for (Object m : methodList) { - InsnList nodes = ((MethodNode) m).instructions; - for (int i = 0, n = nodes.size(); i < n; i++) { - AbstractInsnNode instruction = nodes.get(i); - if (instruction.getOpcode() != Opcodes.INVOKESTATIC){ - continue; - } - MethodInsnNode node = (MethodInsnNode) instruction; - if (node.name.equals(method.name) - && node.desc.equals(method.desc) - && node.owner.equals(classNode.name)) { - // Find lookup switch - AbstractInsnNode target = getNextInstruction(node); - while (target != null) { - if (target.getOpcode() == Opcodes.LOOKUPSWITCH) { - LookupSwitchInsnNode lookup = (LookupSwitchInsnNode) target; - @SuppressWarnings("unchecked") // ASM API - List<Integer> keys = lookup.keys; - if (keys != null && keys.contains(ordinal)) { - String fqcn = ClassContext.getFqcn(owner) + '#' + name; - String message = String.format( - "Enum value requires API level %1$d " + - "(current min is %2$d): %3$s", - api, minSdk, fqcn); - report(context, message, lookup, (MethodNode) m, name, null, - SearchHints.create(FORWARD).matchJavaSymbol()); - - // Break out of the inner target search only; the switch - // statement could be used in other places in this class as - // well and we want to report all problematic usages. - break; - } - } - target = getNextInstruction(target); - } - } - } - } - } - - /** - * Return the {@code @TargetApi} level to use for the given {@code classNode}; - * this will be the {@code @TargetApi} annotation on the class, or any outer - * methods (for anonymous inner classes) or outer classes (for inner classes) - * of the given class. - */ - private static int getClassMinSdk(ClassContext context, ClassNode classNode) { - int classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations); - if (classMinSdk != -1) { - return classMinSdk; - } - - LintDriver driver = context.getDriver(); - while (classNode != null) { - ClassNode prev = classNode; - classNode = driver.getOuterClassNode(classNode); - if (classNode != null) { - // TODO: Should this be "curr" instead? - if (prev.outerMethod != null) { - @SuppressWarnings("rawtypes") // ASM API - List methods = classNode.methods; - for (Object m : methods) { - MethodNode method = (MethodNode) m; - if (method.name.equals(prev.outerMethod) - && method.desc.equals(prev.outerMethodDesc)) { - // Found the outer method for this anonymous class; check method - // annotations on it, then continue up the class hierarchy - int methodMinSdk = getLocalMinSdk(method.invisibleAnnotations); - if (methodMinSdk != -1) { - return methodMinSdk; - } - - break; - } - } - } - - classMinSdk = getLocalMinSdk(classNode.invisibleAnnotations); - if (classMinSdk != -1) { - return classMinSdk; - } - } - } - - return -1; - } - - /** - * Returns the minimum SDK to use according to the given annotation list, or - * -1 if no annotation was found. - * - * @param annotations a list of annotation nodes from ASM - * @return the API level to use for this node, or -1 - */ - @SuppressWarnings({"unchecked", "rawtypes"}) - private static int getLocalMinSdk(List annotations) { - if (annotations != null) { - for (AnnotationNode annotation : (List<AnnotationNode>)annotations) { - String desc = annotation.desc; - if (desc.endsWith(TARGET_API_VMSIG)) { - if (annotation.values != null) { - for (int i = 0, n = annotation.values.size(); i < n; i += 2) { - String key = (String) annotation.values.get(i); - if (key.equals("value")) { //$NON-NLS-1$ - Object value = annotation.values.get(i + 1); - if (value instanceof Integer) { - return ((Integer) value).intValue(); - } - } - } - } - } - } - } - - return -1; - } - - /** - * Returns the minimum SDK to use in the given element context, or -1 if no - * {@code tools:targetApi} attribute was found. - * - * @param element the element to look at, including parents - * @return the API level to use for this element, or -1 - */ - private static int getLocalMinSdk(@NonNull Element element) { - while (element != null) { - String targetApi = element.getAttributeNS(TOOLS_URI, ATTR_TARGET_API); - if (targetApi != null && !targetApi.isEmpty()) { - if (Character.isDigit(targetApi.charAt(0))) { - try { - return Integer.parseInt(targetApi); - } catch (NumberFormatException nufe) { - break; - } - } - - for (int api = 1; api <= SdkConstants.HIGHEST_KNOWN_API; api++) { - String code = LintUtils.getBuildCode(api); - if (code != null && code.equalsIgnoreCase(targetApi)) { - return api; - } - } - } - - Node parent = element.getParentNode(); - if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) { - element = (Element) parent; - } else { - break; - } - } - - return -1; - } - - private static void report(final ClassContext context, String message, AbstractInsnNode node, - MethodNode method, String patternStart, String patternEnd, SearchHints hints) { - int lineNumber = node != null ? ClassContext.findLineNumber(node) : -1; - - // If looking for a constructor, the string we'll see in the source is not the - // method name (<init>) but the class name - if (patternStart != null && patternStart.equals(CONSTRUCTOR_NAME) - && node instanceof MethodInsnNode) { - if (hints != null) { - hints = hints.matchConstructor(); - } - patternStart = ((MethodInsnNode) node).owner; - } - - if (patternStart != null) { - int index = patternStart.lastIndexOf('$'); - if (index != -1) { - patternStart = patternStart.substring(index + 1); - } - index = patternStart.lastIndexOf('/'); - if (index != -1) { - patternStart = patternStart.substring(index + 1); - } - } - - Location location = context.getLocationForLine(lineNumber, patternStart, patternEnd, - hints); - context.report(UNSUPPORTED, method, node, location, message, null); - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mPendingFields != null) { - for (List<Pair<String, Location>> list : mPendingFields.values()) { - for (Pair<String, Location> pair : list) { - String message = pair.getFirst(); - Location location = pair.getSecond(); - context.report(INLINED, location, message, null); - } - } - } - - super.afterCheckProject(context); - } - -// ---- Implements JavaScanner ---- - - @Nullable - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new ApiVisitor(context); - } - - @Nullable - @Override - public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() { - List<Class<? extends lombok.ast.Node>> types = - new ArrayList<Class<? extends lombok.ast.Node>>(2); - types.add(ImportDeclaration.class); - types.add(Select.class); - types.add(MethodDeclaration.class); - types.add(ConstructorDeclaration.class); - types.add(VariableDefinitionEntry.class); - types.add(VariableReference.class); - return types; - } - - /** - * Checks whether the given instruction is a benign usage of a constant defined in - * a later version of Android than the application's {@code minSdkVersion}. - * - * @param node the instruction to check - * @param name the name of the constant - * @param owner the field owner - * @return true if the given usage is safe on older versions than the introduction - * level of the constant - */ - public boolean isBenignConstantUsage( - @Nullable lombok.ast.Node node, - @NonNull String name, - @NonNull String owner) { - if (owner.equals("android/os/Build$VERSION_CODES")) { //$NON-NLS-1$ - // These constants are required for compilation, not execution - // and valid code checks it even on older platforms - return true; - } - if (owner.equals("android/view/ViewGroup$LayoutParams") //$NON-NLS-1$ - && name.equals("MATCH_PARENT")) { //$NON-NLS-1$ - return true; - } - if (owner.equals("android/widget/AbsListView") //$NON-NLS-1$ - && ((name.equals("CHOICE_MODE_NONE") //$NON-NLS-1$ - || name.equals("CHOICE_MODE_MULTIPLE") //$NON-NLS-1$ - || name.equals("CHOICE_MODE_SINGLE")))) { //$NON-NLS-1$ - // android.widget.ListView#CHOICE_MODE_MULTIPLE and friends have API=1, - // but in API 11 it was moved up to the parent class AbsListView. - // Referencing AbsListView#CHOICE_MODE_MULTIPLE technically requires API 11, - // but the constant is the same as the older version, so accept this without - // warning. - return true; - } - - if (node == null) { - return false; - } - - // It's okay to reference the constant as a case constant (since that - // code path won't be taken) or in a condition of an if statement - lombok.ast.Node curr = node.getParent(); - while (curr != null) { - Class<? extends lombok.ast.Node> nodeType = curr.getClass(); - if (nodeType == Case.class) { - Case caseStatement = (Case) curr; - Expression condition = caseStatement.astCondition(); - return condition != null && isAncestor(condition, node); - } else if (nodeType == If.class) { - If ifStatement = (If) curr; - Expression condition = ifStatement.astCondition(); - return condition != null && isAncestor(condition, node); - } else if (nodeType == InlineIfExpression.class) { - InlineIfExpression ifStatement = (InlineIfExpression) curr; - Expression condition = ifStatement.astCondition(); - return condition != null && isAncestor(condition, node); - } - curr = curr.getParent(); - } - - return false; - } - - private static boolean isAncestor( - @NonNull lombok.ast.Node ancestor, - @Nullable lombok.ast.Node node) { - while (node != null) { - if (node == ancestor) { - return true; - } - node = node.getParent(); - } - - return false; - } - - private final class ApiVisitor extends ForwardingAstVisitor { - private JavaContext mContext; - private Map<String, String> mClassToImport = Maps.newHashMap(); - private List<String> mStarImports; - private Set<String> mLocalVars; - private lombok.ast.Node mCurrentMethod; - private Set<String> mFields; - private List<String> mStaticStarImports; - - private ApiVisitor(JavaContext context) { - mContext = context; - } - - @Override - public boolean visitImportDeclaration(ImportDeclaration node) { - if (node.astStarImport()) { - // Similarly, if you're inheriting from a constants class, figure out - // how that works... :=( - String fqcn = node.asFullyQualifiedName(); - int strip = fqcn.lastIndexOf('*'); - if (strip != -1) { - strip = fqcn.lastIndexOf('.', strip); - if (strip != -1) { - String pkgName = getInternalName(fqcn.substring(0, strip)); - if (ApiLookup.isRelevantOwner(pkgName)) { - if (node.astStaticImport()) { - if (mStaticStarImports == null) { - mStaticStarImports = Lists.newArrayList(); - } - mStaticStarImports.add(pkgName); - } else { - if (mStarImports == null) { - mStarImports = Lists.newArrayList(); - } - mStarImports.add(pkgName); - } - } - } - } - } else if (node.astStaticImport()) { - String fqcn = node.asFullyQualifiedName(); - String fieldName = getInternalName(fqcn); - int index = fieldName.lastIndexOf('$'); - if (index != -1) { - String owner = fieldName.substring(0, index); - String name = fieldName.substring(index + 1); - checkField(node, name, owner); - } - } else { - // Store in map -- if it's "one of ours" - // Use override detector's map for that purpose - String fqcn = node.asFullyQualifiedName(); - - int last = fqcn.lastIndexOf('.'); - if (last != -1) { - String className = fqcn.substring(last + 1); - mClassToImport.put(className, fqcn); - } - } - - return super.visitImportDeclaration(node); - } - - @Override - public boolean visitSelect(Select node) { - boolean result = super.visitSelect(node); - - if (node.getParent() instanceof Select) { - // We only want to look at the leaf expressions; e.g. if you have - // "foo.bar.baz" we only care about the select foo.bar.baz, not foo.bar - return result; - } - - // See if this corresponds to a field reference. We assume it's a field if - // it's a select (x.y) and either the identifier y is capitalized (e.g. - // foo.VIEW_MASK) or if it's a member of an R class (R.id.foo). - String name = node.astIdentifier().astValue(); - boolean isField = Character.isUpperCase(name.charAt(0)); - if (!isField) { - // See if there's an R class - Select current = node; - while (current != null) { - Expression operand = current.astOperand(); - if (operand instanceof Select) { - current = (Select) operand; - if (R_CLASS.equals(current.astIdentifier().astValue())) { - isField = true; - break; - } - } else if (operand instanceof VariableReference) { - VariableReference reference = (VariableReference) operand; - if (R_CLASS.equals(reference.astIdentifier().astValue())) { - isField = true; - } - break; - } else { - break; - } - } - } - - if (isField) { - Expression operand = node.astOperand(); - if (operand.getClass() == Select.class) { - // Possibly a fully qualified name in place - String cls = operand.toString(); - - // See if it's an imported class with an inner class - // (e.g. Manifest.permission.FIELD) - if (Character.isUpperCase(cls.charAt(0))) { - int firstDot = cls.indexOf('.'); - if (firstDot != -1) { - String base = cls.substring(0, firstDot); - String fqcn = mClassToImport.get(base); - if (fqcn != null) { - // Yes imported - String owner = getInternalName(fqcn + cls.substring(firstDot)); - checkField(node, name, owner); - return result; - } - - // Might be a star import: have to iterate and check here - if (mStarImports != null) { - for (String packagePrefix : mStarImports) { - String owner = getInternalName(packagePrefix + '/' + cls); - if (checkField(node, name, owner)) { - mClassToImport.put(name, owner); - return result; - } - } - } - } - } - - // See if it's a fully qualified reference in place - String owner = getInternalName(cls); - checkField(node, name, owner); - return result; - } else if (operand.getClass() == VariableReference.class) { - String className = ((VariableReference) operand).astIdentifier().astValue(); - // Not a FQCN that we care about: look in imports - String fqcn = mClassToImport.get(className); - if (fqcn != null) { - // Yes imported - String owner = getInternalName(fqcn); - checkField(node, name, owner); - return result; - } - - if (Character.isUpperCase(className.charAt(0))) { - // Might be a star import: have to iterate and check here - if (mStarImports != null) { - for (String packagePrefix : mStarImports) { - String owner = getInternalName(packagePrefix) + '/' + className; - if (checkField(node, name, owner)) { - mClassToImport.put(name, owner); - return result; - } - } - } - } - } - } - return result; - } - - @Override - public boolean visitVariableReference(VariableReference node) { - boolean result = super.visitVariableReference(node); - - if (node.getParent() != null) { - lombok.ast.Node parent = node.getParent(); - Class<? extends lombok.ast.Node> parentClass = parent.getClass(); - if (parentClass == Select.class - || parentClass == Switch.class // look up on the switch expression type - || parentClass == Case.class - || parentClass == ConstructorInvocation.class - || parentClass == SuperConstructorInvocation.class - || parentClass == AnnotationElement.class) { - return result; - } - - if (parent instanceof MethodInvocation && - ((MethodInvocation) parent).astOperand() == node) { - return result; - } else if (parent instanceof BinaryExpression) { - BinaryExpression expression = (BinaryExpression) parent; - if (expression.astLeft() == node) { - return result; - } - } - } - - String name = node.astIdentifier().astValue(); - if (Character.isUpperCase(name.charAt(0)) - && (mLocalVars == null || !mLocalVars.contains(name)) - && (mFields == null || !mFields.contains(name))) { - // Potential field reference: check it - if (mStaticStarImports != null) { - for (String owner : mStaticStarImports) { - if (checkField(node, name, owner)) { - break; - } - } - } - } - - return result; - } - - @Override - public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { - if (mCurrentMethod != null) { - if (mLocalVars == null) { - mLocalVars = Sets.newHashSet(); - } - mLocalVars.add(node.astName().astValue()); - } else { - if (mFields == null) { - mFields = Sets.newHashSet(); - } - mFields.add(node.astName().astValue()); - } - return super.visitVariableDefinitionEntry(node); - } - - @Override - public boolean visitMethodDeclaration(MethodDeclaration node) { - mLocalVars = null; - mCurrentMethod = node; - return super.visitMethodDeclaration(node); - } - - @Override - public boolean visitConstructorDeclaration(ConstructorDeclaration node) { - mLocalVars = null; - mCurrentMethod = node; - return super.visitConstructorDeclaration(node); - } - - @Override - public void endVisit(lombok.ast.Node node) { - if (node == mCurrentMethod) { - mCurrentMethod = null; - } - super.endVisit(node); - } - - /** - * Checks a Java source field reference. Returns true if the field is known - * regardless of whether it's an invalid field or not - */ - private boolean checkField( - @NonNull lombok.ast.Node node, - @NonNull String name, - @NonNull String owner) { - int api = mApiDatabase.getFieldVersion(owner, name); - if (api != -1) { - int minSdk = getMinSdk(mContext); - if (api > minSdk - && api > getLocalMinSdk(node)) { - if (isBenignConstantUsage(node, name, owner)) { - return true; - } - - Location location = mContext.getLocation(node); - String fqcn = getFqcn(owner) + '#' + name; - - if (node instanceof ImportDeclaration) { - // Replace import statement location range with just - // the identifier part - ImportDeclaration d = (ImportDeclaration) node; - int startOffset = d.astParts().first().getPosition().getStart(); - Position start = location.getStart(); - int startColumn = start.getColumn(); - int startLine = start.getLine(); - start = new DefaultPosition(startLine, - startColumn + startOffset - start.getOffset(), startOffset); - int fqcnLength = fqcn.length(); - Position end = new DefaultPosition(startLine, - start.getColumn() + fqcnLength, - start.getOffset() + fqcnLength); - location = Location.create(location.getFile(), start, end); - } - - String message = String.format( - "Field requires API level %1$d (current min is %2$d): %3$s", - api, minSdk, fqcn); - - LintDriver driver = mContext.getDriver(); - if (driver.isSuppressed(INLINED, node)) { - return true; - } - - // Also allow to suppress these issues with NewApi, since some - // fields used to get identified that way - if (driver.isSuppressed(UNSUPPORTED, node)) { - return true; - } - - // We can't report the issue right away; we don't yet know if - // this is an actual inlined (static primitive or String) yet. - // So just make a note of it, and report these after the project - // checking has finished; any fields that aren't inlined will be - // cleared when they're noticed by the class check. - if (mPendingFields == null) { - mPendingFields = Maps.newHashMapWithExpectedSize(20); - } - List<Pair<String, Location>> list = mPendingFields.get(fqcn); - if (list == null) { - list = new ArrayList<Pair<String, Location>>(); - mPendingFields.put(fqcn, list); - } - list.add(Pair.of(message, location)); - } - - return true; - } - - return false; - } - - /** - * Returns the minimum SDK to use according to the given AST node, or null - * if no {@code TargetApi} annotations were found - * - * @return the API level to use for this node, or -1 - */ - public int getLocalMinSdk(@Nullable lombok.ast.Node scope) { - while (scope != null) { - Class<? extends lombok.ast.Node> type = scope.getClass(); - // The Lombok AST uses a flat hierarchy of node type implementation classes - // so no need to do instanceof stuff here. - if (type == VariableDefinition.class) { - // Variable - VariableDefinition declaration = (VariableDefinition) scope; - int targetApi = getLocalMinSdk(declaration.astModifiers()); - if (targetApi != -1) { - return targetApi; - } - } else if (type == MethodDeclaration.class) { - // Method - // Look for annotations on the method - MethodDeclaration declaration = (MethodDeclaration) scope; - int targetApi = getLocalMinSdk(declaration.astModifiers()); - if (targetApi != -1) { - return targetApi; - } - } else if (type == ConstructorDeclaration.class) { - // Constructor - // Look for annotations on the method - ConstructorDeclaration declaration = (ConstructorDeclaration) scope; - int targetApi = getLocalMinSdk(declaration.astModifiers()); - if (targetApi != -1) { - return targetApi; - } - } else if (type == ClassDeclaration.class) { - // Class - ClassDeclaration declaration = (ClassDeclaration) scope; - int targetApi = getLocalMinSdk(declaration.astModifiers()); - if (targetApi != -1) { - return targetApi; - } - } - - scope = scope.getParent(); - } - - return -1; - } - - /** - * Returns true if the given AST modifier has a suppress annotation for the - * given issue (which can be null to check for the "all" annotation) - * - * @param modifiers the modifier to check - * @return true if the issue or all issues should be suppressed for this - * modifier - */ - private int getLocalMinSdk(@Nullable Modifiers modifiers) { - if (modifiers == null) { - return -1; - } - StrictListAccessor<Annotation, Modifiers> annotations = modifiers.astAnnotations(); - if (annotations == null) { - return -1; - } - - Iterator<Annotation> iterator = annotations.iterator(); - while (iterator.hasNext()) { - Annotation annotation = iterator.next(); - TypeReference t = annotation.astAnnotationTypeReference(); - String typeName = t.getTypeName(); - if (typeName.endsWith(TARGET_API)) { - StrictListAccessor<AnnotationElement, Annotation> values = - annotation.astElements(); - if (values != null) { - Iterator<AnnotationElement> valueIterator = values.iterator(); - while (valueIterator.hasNext()) { - AnnotationElement element = valueIterator.next(); - AnnotationValue valueNode = element.astValue(); - if (valueNode == null) { - continue; - } - if (valueNode instanceof IntegralLiteral) { - IntegralLiteral literal = (IntegralLiteral) valueNode; - return literal.astIntValue(); - } else if (valueNode instanceof StringLiteral) { - String value = ((StringLiteral) valueNode).astValue(); - return codeNameToApi(value); - } else if (valueNode instanceof Select) { - Select select = (Select) valueNode; - String codename = select.astIdentifier().astValue(); - return codeNameToApi(codename); - } else if (valueNode instanceof VariableReference) { - VariableReference reference = (VariableReference) valueNode; - String codename = reference.astIdentifier().astValue(); - return codeNameToApi(codename); - } - } - } - } - } - - return -1; - } - } - - private static int codeNameToApi(String codename) { - for (int api = 1; api <= SdkConstants.HIGHEST_KNOWN_API; api++) { - String code = LintUtils.getBuildCode(api); - if (code != null && code.equalsIgnoreCase(codename)) { - return api; - } - } - - return -1; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java deleted file mode 100644 index 6a4de6e..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiLookup.java +++ /dev/null @@ -1,971 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_PKG; -import static com.android.SdkConstants.DOT_XML; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.tools.lint.client.api.LintClient; -import com.android.tools.lint.detector.api.LintUtils; -import com.google.common.base.Charsets; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; -import com.google.common.io.Files; -import com.google.common.primitives.UnsignedBytes; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.lang.ref.WeakReference; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel.MapMode; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Database for API checking: Allows quick lookup of a given class, method or field - * to see which API level it was introduced in. - * <p> - * This class is optimized for quick bytecode lookup used in conjunction with the - * ASM library: It has lookup methods that take internal JVM signatures, and for a method - * call for example it processes the owner, name and description parameters separately - * the way they are provided from ASM. - * <p> - * The {@link Api} class provides access to the full Android API along with version - * information, initialized from an XML file. This lookup class adds a binary cache around - * the API to make initialization faster and to require fewer objects. It creates - * a binary cache data structure, which fits in a single byte array, which means that - * to open the database you can just read in the byte array and go. On one particular - * machine, this takes about 30-50 ms versus 600-800ms for the full parse. It also - * helps memory by placing everything in a compact byte array instead of needing separate - * strings (2 bytes per character in a char[] for the 25k method entries, 11k field entries - * and 6k class entries) - and it also avoids the same number of Map.Entry objects. - * When creating the memory data structure it performs a few other steps to help memory: - * <ul> - * <li> It stores the strings as single bytes, since all the JVM signatures are in ASCII - * <li> It strips out the method return types (which takes the binary size down from - * about 4.7M to 4.0M) - * <li> It strips out all APIs that have since=1, since the lookup only needs to find - * classes, methods and fields that have an API level *higher* than 1. This drops - * the memory use down from 4.0M to 1.7M. - * </ul> - */ -public class ApiLookup { - /** Relative path to the api-versions.xml database file within the Lint installation */ - private static final String XML_FILE_PATH = "platform-tools/api/api-versions.xml"; //$NON-NLS-1$ - private static final String FILE_HEADER = "API database used by Android lint\000"; - private static final int BINARY_FORMAT_VERSION = 5; - private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false; - private static final boolean DEBUG_SEARCH = false; - private static final boolean WRITE_STATS = false; - /** Default size to reserve for each API entry when creating byte buffer to build up data */ - private static final int BYTES_PER_ENTRY = 36; - - private final LintClient mClient; - private final File mXmlFile; - private final File mBinaryFile; - private final Api mInfo; - private byte[] mData; - private int[] mIndices; - private int mClassCount; - private String[] mJavaPackages; - - private static WeakReference<ApiLookup> sInstance = - new WeakReference<ApiLookup>(null); - - /** - * Returns an instance of the API database - * - * @param client the client to associate with this database - used only for - * logging. The database object may be shared among repeated invocations, - * and in that case client used will be the one originally passed in. - * In other words, this parameter may be ignored if the client created - * is not new. - * @return a (possibly shared) instance of the API database, or null - * if its data can't be found - */ - public static ApiLookup get(LintClient client) { - synchronized (ApiLookup.class) { - ApiLookup db = sInstance.get(); - if (db == null) { - File file = client.findResource(XML_FILE_PATH); - if (file == null) { - // AOSP build environment? - String build = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ - if (build != null) { - file = new File(build, "development/sdk/api-versions.xml" //$NON-NLS-1$ - .replace('/', File.separatorChar)); - } - } - - if (file == null || !file.exists()) { - client.log(null, "Fatal error: No API database found at %1$s", file); - return null; - } else { - db = get(client, file); - } - sInstance = new WeakReference<ApiLookup>(db); - } - - return db; - } - } - - @VisibleForTesting - static String getCacheFileName(String xmlFileName) { - if (LintUtils.endsWith(xmlFileName, DOT_XML)) { - xmlFileName = xmlFileName.substring(0, xmlFileName.length() - DOT_XML.length()); - } - - // Incorporate version number in the filename to avoid upgrade filename - // conflicts on Windows (such as issue #26663) - return xmlFileName + '-' + BINARY_FORMAT_VERSION + ".bin"; //$NON-NLS-1$ - } - - /** - * Returns an instance of the API database - * - * @param client the client to associate with this database - used only for - * logging - * @param xmlFile the XML file containing configuration data to use for this - * database - * @return a (possibly shared) instance of the API database, or null - * if its data can't be found - */ - private static ApiLookup get(LintClient client, File xmlFile) { - if (!xmlFile.exists()) { - client.log(null, "The API database file %1$s does not exist", xmlFile); - return null; - } - - File cacheDir = client.getCacheDir(true/*create*/); - if (cacheDir == null) { - cacheDir = xmlFile.getParentFile(); - } - - File binaryData = new File(cacheDir, getCacheFileName(xmlFile.getName())); - - if (DEBUG_FORCE_REGENERATE_BINARY) { - System.err.println("\nTemporarily regenerating binary data unconditionally \nfrom " - + xmlFile + "\nto " + binaryData); - if (!createCache(client, xmlFile, binaryData)) { - return null; - } - } else if (!binaryData.exists() || binaryData.lastModified() < xmlFile.lastModified() - || binaryData.length() == 0) { - if (!createCache(client, xmlFile, binaryData)) { - return null; - } - } - - if (!binaryData.exists()) { - client.log(null, "The API database file %1$s does not exist", binaryData); - return null; - } - - return new ApiLookup(client, xmlFile, binaryData, null); - } - - private static boolean createCache(LintClient client, File xmlFile, File binaryData) { - long begin = 0; - if (WRITE_STATS) { - begin = System.currentTimeMillis(); - } - - Api info = Api.parseApi(xmlFile); - - if (WRITE_STATS) { - long end = System.currentTimeMillis(); - System.out.println("Reading XML data structures took " + (end - begin) + " ms)"); - } - - if (info != null) { - try { - writeDatabase(binaryData, info); - return true; - } catch (IOException ioe) { - client.log(ioe, "Can't write API cache file"); - } - } - - return false; - } - - /** Use one of the {@link #get} factory methods instead */ - private ApiLookup( - @NonNull LintClient client, - @NonNull File xmlFile, - @Nullable File binaryFile, - @Nullable Api info) { - mClient = client; - mXmlFile = xmlFile; - mBinaryFile = binaryFile; - mInfo = info; - - if (binaryFile != null) { - readData(); - } - } - - /** - * Database format: - * <pre> - * 1. A file header, which is the exact contents of {@link #FILE_HEADER} encoded - * as ASCII characters. The purpose of the header is to identify what the file - * is for, for anyone attempting to open the file. - * 2. A file version number. If the binary file does not match the reader's expected - * version, it can ignore it (and regenerate the cache from XML). - * 3. The number of classes [1 int] - * 4. The number of members (across all classes) [1 int]. - * 5. The number of java/javax packages [1 int] - * 6. The java/javax package name table. Each item consists of a byte count for - * the package string (as 1 byte) followed by the UTF-8 encoded bytes for each package. - * These are in sorted order. - * 7. Class offset table (one integer per class, pointing to the byte offset in the - * file (relative to the beginning of the file) where each class begins. - * The classes are always sorted alphabetically by fully qualified name. - * 8. Member offset table (one integer per member, pointing to the byte offset in the - * file (relative to the beginning of the file) where each member entry begins. - * The members are always sorted alphabetically. - * 9. Class entry table. Each class entry consists of the fully qualified class name, - * in JVM format (using / instead of . in package names and $ for inner classes), - * followed by the byte 0 as a terminator, followed by the API version as a byte. - * 10. Member entry table. Each member entry consists of the class number (as a short), - * followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte - * signature terminator, followed by the API level as a byte. - * <p> - * TODO: Pack the offsets: They increase by a small amount for each entry, so no need - * to spend 4 bytes on each. These will need to be processed when read back in anyway, - * so consider storing the offset -deltas- as single bytes and adding them up cumulatively - * in readData(). - * </pre> - */ - private void readData() { - if (!mBinaryFile.exists()) { - mClient.log(null, "%1$s does not exist", mBinaryFile); - return; - } - long start = System.currentTimeMillis(); - try { - MappedByteBuffer buffer = Files.map(mBinaryFile, MapMode.READ_ONLY); - assert buffer.order() == ByteOrder.BIG_ENDIAN; - - // First skip the header - byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII); - buffer.rewind(); - for (int offset = 0; offset < expectedHeader.length; offset++) { - if (expectedHeader[offset] != buffer.get()) { - mClient.log(null, "Incorrect file header: not an API database cache " + - "file, or a corrupt cache file"); - return; - } - } - - // Read in the format number - if (buffer.get() != BINARY_FORMAT_VERSION) { - // Force regeneration of new binary data with up to date format - if (createCache(mClient, mXmlFile, mBinaryFile)) { - readData(); // Recurse - } - - return; - } - - mClassCount = buffer.getInt(); - int methodCount = buffer.getInt(); - - int javaPackageCount = buffer.getInt(); - // Read in the Java packages - mJavaPackages = new String[javaPackageCount]; - for (int i = 0; i < javaPackageCount; i++) { - int count = UnsignedBytes.toInt(buffer.get()); - byte[] bytes = new byte[count]; - buffer.get(bytes, 0, count); - mJavaPackages[i] = new String(bytes, Charsets.UTF_8); - } - - // Read in the class table indices; - int count = mClassCount + methodCount; - int[] offsets = new int[count]; - - // Another idea: I can just store the DELTAS in the file (and add them up - // when reading back in) such that it takes just ONE byte instead of four! - - for (int i = 0; i < count; i++) { - offsets[i] = buffer.getInt(); - } - - // No need to read in the rest -- we'll just keep the whole byte array in memory - // TODO: Make this code smarter/more efficient. - int size = buffer.limit(); - byte[] b = new byte[size]; - buffer.rewind(); - buffer.get(b); - mData = b; - mIndices = offsets; - - // TODO: We only need to keep the data portion here since we've initialized - // the offset array separately. - // TODO: Investigate (profile) accessing the byte buffer directly instead of - // accessing a byte array. - } catch (Throwable e) { - mClient.log(null, "Failure reading binary cache file %1$s", mBinaryFile.getPath()); - mClient.log(null, "Please delete the file and restart the IDE/lint: %1$s", - mBinaryFile.getPath()); - mClient.log(e, null); - } - if (WRITE_STATS) { - long end = System.currentTimeMillis(); - System.out.println("\nRead API database in " + (end - start) - + " milliseconds."); - System.out.println("Size of data table: " + mData.length + " bytes (" - + Integer.toString(mData.length / 1024) + "k)\n"); - } - } - - /** See the {@link #readData()} for documentation on the data format. */ - private static void writeDatabase(File file, Api info) throws IOException { - /* - * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - * as ASCII characters. The purpose of the header is to identify what the file - * is for, for anyone attempting to open the file. - * 2. A file version number. If the binary file does not match the reader's expected - * version, it can ignore it (and regenerate the cache from XML). - */ - Map<String, ApiClass> classMap = info.getClasses(); - // Write the class table - - List<String> classes = new ArrayList<String>(classMap.size()); - Map<ApiClass, List<String>> memberMap = - Maps.newHashMapWithExpectedSize(classMap.size()); - int memberCount = 0; - Set<String> javaPackageSet = Sets.newHashSetWithExpectedSize(70); - for (Map.Entry<String, ApiClass> entry : classMap.entrySet()) { - String className = entry.getKey(); - ApiClass apiClass = entry.getValue(); - - if (className.startsWith("java/") //$NON-NLS-1$ - || className.startsWith("javax/")) { //$NON-NLS-1$ - String pkg = apiClass.getPackage(); - javaPackageSet.add(pkg); - } - - if (!isRelevantOwner(className)) { - System.out.println("Warning: The isRelevantOwner method does not pass " - + className); - } - - Set<String> allMethods = apiClass.getAllMethods(info); - Set<String> allFields = apiClass.getAllFields(info); - - // Strip out all members that have been supported since version 1. - // This makes the database *much* leaner (down from about 4M to about - // 1.7M), and this just fills the table with entries that ultimately - // don't help the API checker since it just needs to know if something - // requires a version *higher* than the minimum. If in the future the - // database needs to answer queries about whether a method is public - // or not, then we'd need to put this data back in. - List<String> members = new ArrayList<String>(allMethods.size() + allFields.size()); - for (String member : allMethods) { - - Integer since = apiClass.getMethod(member, info); - if (since == null) { - assert false : className + ':' + member; - since = 1; - } - if (since != 1) { - members.add(member); - } - } - - // Strip out all members that have been supported since version 1. - // This makes the database *much* leaner (down from about 4M to about - // 1.7M), and this just fills the table with entries that ultimately - // don't help the API checker since it just needs to know if something - // requires a version *higher* than the minimum. If in the future the - // database needs to answer queries about whether a method is public - // or not, then we'd need to put this data back in. - for (String member : allFields) { - Integer since = apiClass.getField(member, info); - if (since == null) { - assert false : className + ':' + member; - since = 1; - } - if (since != 1) { - members.add(member); - } - } - - // Only include classes that have one or more members requiring version 2 or higher: - if (!members.isEmpty()) { - classes.add(className); - memberMap.put(apiClass, members); - memberCount += members.size(); - } - } - Collections.sort(classes); - - List<String> javaPackages = Lists.newArrayList(javaPackageSet); - Collections.sort(javaPackages); - int javaPackageCount = javaPackages.size(); - - int entryCount = classMap.size() + memberCount; - int capacity = entryCount * BYTES_PER_ENTRY; - ByteBuffer buffer = ByteBuffer.allocate(capacity); - buffer.order(ByteOrder.BIG_ENDIAN); - // 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - // as ASCII characters. The purpose of the header is to identify what the file - // is for, for anyone attempting to open the file. - - buffer.put(FILE_HEADER.getBytes(Charsets.US_ASCII)); - - // 2. A file version number. If the binary file does not match the reader's expected - // version, it can ignore it (and regenerate the cache from XML). - buffer.put((byte) BINARY_FORMAT_VERSION); - - // 3. The number of classes [1 int] - buffer.putInt(classes.size()); - - // 4. The number of members (across all classes) [1 int]. - buffer.putInt(memberCount); - - // 5. The number of Java packages [1 int]. - buffer.putInt(javaPackageCount); - - // 6. The Java package table. There are javaPackage.size() entries, where each entry - // consists of a string length, as a byte, followed by the bytes in the package. - // There is no terminating 0. - for (String pkg : javaPackages) { - byte[] bytes = pkg.getBytes(Charsets.UTF_8); - assert bytes.length < 255 : pkg; - buffer.put((byte) bytes.length); - buffer.put(bytes); - } - - // 7. Class offset table (one integer per class, pointing to the byte offset in the - // file (relative to the beginning of the file) where each class begins. - // The classes are always sorted alphabetically by fully qualified name. - int classOffsetTable = buffer.position(); - - // Reserve enough room for the offset table here: we will backfill it with pointers - // as we're writing out the data structures below - for (int i = 0, n = classes.size(); i < n; i++) { - buffer.putInt(0); - } - - // 8. Member offset table (one integer per member, pointing to the byte offset in the - // file (relative to the beginning of the file) where each member entry begins. - // The members are always sorted alphabetically. - int methodOffsetTable = buffer.position(); - for (int i = 0, n = memberCount; i < n; i++) { - buffer.putInt(0); - } - - int nextEntry = buffer.position(); - int nextOffset = classOffsetTable; - - // 9. Class entry table. Each class entry consists of the fully qualified class name, - // in JVM format (using / instead of . in package names and $ for inner classes), - // followed by the byte 0 as a terminator, followed by the API version as a byte. - for (String clz : classes) { - buffer.position(nextOffset); - buffer.putInt(nextEntry); - nextOffset = buffer.position(); - buffer.position(nextEntry); - buffer.put(clz.getBytes(Charsets.UTF_8)); - buffer.put((byte) 0); - - ApiClass apiClass = classMap.get(clz); - assert apiClass != null : clz; - int since = apiClass.getSince(); - assert since == UnsignedBytes.toInt((byte) since) : since; // make sure it fits - buffer.put((byte) since); - - nextEntry = buffer.position(); - } - - // 10. Member entry table. Each member entry consists of the class number (as a short), - // followed by the JVM method/field signature, encoded as UTF-8, followed by a 0 byte - // signature terminator, followed by the API level as a byte. - assert nextOffset == methodOffsetTable; - - for (int classNumber = 0, n = classes.size(); classNumber < n; classNumber++) { - String clz = classes.get(classNumber); - ApiClass apiClass = classMap.get(clz); - assert apiClass != null : clz; - List<String> members = memberMap.get(apiClass); - Collections.sort(members); - - for (String member : members) { - buffer.position(nextOffset); - buffer.putInt(nextEntry); - nextOffset = buffer.position(); - buffer.position(nextEntry); - - Integer since; - if (member.indexOf('(') != -1) { - since = apiClass.getMethod(member, info); - } else { - since = apiClass.getField(member, info); - } - if (since == null) { - assert false : clz + ':' + member; - since = 1; - } - - assert classNumber == (short) classNumber; - buffer.putShort((short) classNumber); - byte[] signature = member.getBytes(Charsets.UTF_8); - for (int i = 0; i < signature.length; i++) { - // Make sure all signatures are really just simple ASCII - byte b = signature[i]; - assert b == (b & 0x7f) : member; - buffer.put(b); - // Skip types on methods - if (b == (byte) ')') { - break; - } - } - buffer.put((byte) 0); - int api = since; - assert api == UnsignedBytes.toInt((byte) api); - //assert api >= 1 && api < 0xFF; // max that fits in a byte - buffer.put((byte) api); - nextEntry = buffer.position(); - } - } - - int size = buffer.position(); - assert size <= buffer.limit(); - buffer.mark(); - - if (WRITE_STATS) { - System.out.println("Wrote " + classes.size() + " classes and " - + memberCount + " member entries"); - System.out.print("Actual binary size: " + size + " bytes"); - System.out.println(String.format(" (%.1fM)", size/(1024*1024.f))); - - System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes"); - System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes"); - } - - // Now dump this out as a file - // There's probably an API to do this more efficiently; TODO: Look into this. - byte[] b = new byte[size]; - buffer.rewind(); - buffer.get(b); - if (file.exists()) { - file.delete(); - } - FileOutputStream output = Files.newOutputStreamSupplier(file).getOutput(); - output.write(b); - output.close(); - } - - // For debugging only - private String dumpEntry(int offset) { - if (DEBUG_SEARCH) { - StringBuilder sb = new StringBuilder(200); - for (int i = offset; i < mData.length; i++) { - if (mData[i] == 0) { - break; - } - char c = (char) UnsignedBytes.toInt(mData[i]); - sb.append(c); - } - - return sb.toString(); - } else { - return "<disabled>"; //$NON-NLS-1$ - } - } - - private static int compare(byte[] data, int offset, byte terminator, String s, int max) { - int i = offset; - int j = 0; - for (; j < max; i++, j++) { - byte b = data[i]; - char c = s.charAt(j); - // TODO: Check somewhere that the strings are purely in the ASCII range; if not - // they're not a match in the database - byte cb = (byte) c; - int delta = b - cb; - if (delta != 0) { - return delta; - } - } - - return data[i] - terminator; - } - - /** - * Quick determination whether a given class name is possibly interesting; this - * is a quick package prefix check to determine whether we need to consider - * the class at all. This let's us do less actual searching for the vast majority - * of APIs (in libraries, application code etc) that have nothing to do with the - * APIs in our packages. - * @param name the class name in VM format (e.g. using / instead of .) - * @return true if the owner is <b>possibly</b> relevant - */ - public boolean isRelevantClass(String name) { - // TODO: Add quick switching here. This is tied to the database file so if - // we end up with unexpected prefixes there, this could break. For that reason, - // for now we consider everything relevant. - return true; - } - - /** - * Returns the API version required by the given class reference, - * or -1 if this is not a known API class. Note that it may return -1 - * for classes introduced in version 1; internally the database only - * stores version data for version 2 and up. - * - * @param className the internal name of the class, e.g. its - * fully qualified name (as returned by Class.getName(), but with - * '.' replaced by '/'. - * @return the minimum API version the method is supported for, or -1 if - * it's unknown <b>or version 1</b>. - */ - public int getClassVersion(@NonNull String className) { - if (!isRelevantClass(className)) { - return -1; - } - - if (mData != null) { - int classNumber = findClass(className); - if (classNumber != -1) { - int offset = mIndices[classNumber]; - while (mData[offset] != 0) { - offset++; - } - offset++; - return UnsignedBytes.toInt(mData[offset]); - } - } else { - ApiClass clz = mInfo.getClass(className); - if (clz != null) { - int since = clz.getSince(); - if (since == Integer.MAX_VALUE) { - since = -1; - } - return since; - } - } - - return -1; - } - - /** - * Returns the API version required by the given method call. The method is - * referred to by its {@code owner}, {@code name} and {@code desc} fields. - * If the method is unknown it returns -1. Note that it may return -1 for - * classes introduced in version 1; internally the database only stores - * version data for version 2 and up. - * - * @param owner the internal name of the method's owner class, e.g. its - * fully qualified name (as returned by Class.getName(), but with - * '.' replaced by '/'. - * @param name the method's name - * @param desc the method's descriptor - see {@link org.objectweb.asm.Type} - * @return the minimum API version the method is supported for, or -1 if - * it's unknown <b>or version 1</b>. - */ - public int getCallVersion( - @NonNull String owner, - @NonNull String name, - @NonNull String desc) { - if (!isRelevantClass(owner)) { - return -1; - } - - if (mData != null) { - int classNumber = findClass(owner); - if (classNumber != -1) { - return findMember(classNumber, name, desc); - } - } else { - ApiClass clz = mInfo.getClass(owner); - if (clz != null) { - String signature = name + desc; - int since = clz.getMethod(signature, mInfo); - if (since == Integer.MAX_VALUE) { - since = -1; - } - return since; - } - } - - return -1; - } - - /** - * Returns the API version required to access the given field, or -1 if this - * is not a known API method. Note that it may return -1 for classes - * introduced in version 1; internally the database only stores version data - * for version 2 and up. - * - * @param owner the internal name of the method's owner class, e.g. its - * fully qualified name (as returned by Class.getName(), but with - * '.' replaced by '/'. - * @param name the method's name - * @return the minimum API version the method is supported for, or -1 if - * it's unknown <b>or version 1</b> - */ - public int getFieldVersion( - @NonNull String owner, - @NonNull String name) { - if (!isRelevantClass(owner)) { - return -1; - } - - if (mData != null) { - int classNumber = findClass(owner); - if (classNumber != -1) { - return findMember(classNumber, name, null); - } - } else { - ApiClass clz = mInfo.getClass(owner); - if (clz != null) { - int since = clz.getField(name, mInfo); - if (since == Integer.MAX_VALUE) { - since = -1; - } - return since; - } - } - - return -1; - } - - /** - * Returns true if the given owner (in VM format) is relevant to the database. - * This allows quick filtering out of owners that won't return any data - * for the various {@code #getFieldVersion} etc methods. - * - * @param owner the owner to look up - * @return true if the owner might be relevant to the API database - */ - public static boolean isRelevantOwner(@NonNull String owner) { - if (owner.startsWith("java")) { //$NON-NLS-1$ // includes javax/ - return true; - } - if (owner.startsWith(ANDROID_PKG)) { - if (owner.startsWith("/support/", 7)) { //$NON-NLS-1$ - return false; - } - return true; - } else if (owner.startsWith("org/")) { //$NON-NLS-1$ - if (owner.startsWith("xml", 4) //$NON-NLS-1$ - || owner.startsWith("w3c/", 4) //$NON-NLS-1$ - || owner.startsWith("json/", 4) //$NON-NLS-1$ - || owner.startsWith("apache/", 4)) { //$NON-NLS-1$ - return true; - } - } else if (owner.startsWith("com/")) { //$NON-NLS-1$ - if (owner.startsWith("google/", 4) //$NON-NLS-1$ - || owner.startsWith("android/", 4)) { //$NON-NLS-1$ - return true; - } - } else if (owner.startsWith("junit") //$NON-NLS-1$ - || owner.startsWith("dalvik")) { //$NON-NLS-1$ - return true; - } - - return false; - } - - - /** - * Returns true if the given owner (in VM format) is a valid Java package supported - * in any version of Android. - * - * @param owner the package, in VM format - * @return true if the package is included in one or more versions of Android - */ - public boolean isValidJavaPackage(@NonNull String owner) { - int packageLength = owner.lastIndexOf('/'); - if (packageLength == -1) { - return false; - } - - // The index array contains class indexes from 0 to classCount and - // member indices from classCount to mIndices.length. - int low = 0; - int high = mJavaPackages.length - 1; - while (low <= high) { - int middle = (low + high) >>> 1; - int offset = middle; - - if (DEBUG_SEARCH) { - System.out.println("Comparing string " + owner + " with entry at " + offset - + ": " + mJavaPackages[offset]); - } - - // Compare the api info at the given index. - int compare = comparePackage(mJavaPackages[offset], owner, packageLength); - if (compare == 0) { - return true; - } - - if (compare < 0) { - low = middle + 1; - } else if (compare > 0) { - high = middle - 1; - } else { - assert false; // compare == 0 already handled above - return false; - } - } - - return false; - } - - private static int comparePackage(String s1, String s2, int max) { - for (int i = 0; i < max; i++) { - if (i == s1.length()) { - return -1; - } - char c1 = s1.charAt(i); - char c2 = s2.charAt(i); - if (c1 != c2) { - return c1 - c2; - } - } - - if (s1.length() > max) { - return 1; - } - - return 0; - } - - /** Returns the class number of the given class, or -1 if it is unknown */ - private int findClass(@NonNull String owner) { - assert owner.indexOf('.') == -1 : "Should use / instead of . in owner: " + owner; - - // The index array contains class indexes from 0 to classCount and - // member indices from classCount to mIndices.length. - int low = 0; - int high = mClassCount - 1; - // Compare the api info at the given index. - int classNameLength = owner.length(); - while (low <= high) { - int middle = (low + high) >>> 1; - int offset = mIndices[middle]; - - if (DEBUG_SEARCH) { - System.out.println("Comparing string " + owner + " with entry at " + offset - + ": " + dumpEntry(offset)); - } - - int compare = compare(mData, offset, (byte) 0, owner, classNameLength); - if (compare == 0) { - return middle; - } - - if (compare < 0) { - low = middle + 1; - } else if (compare > 0) { - high = middle - 1; - } else { - assert false; // compare == 0 already handled above - return -1; - } - } - - return -1; - } - - private int findMember(int classNumber, @NonNull String name, @Nullable String desc) { - // The index array contains class indexes from 0 to classCount and - // member indices from classCount to mIndices.length. - int low = mClassCount; - int high = mIndices.length - 1; - while (low <= high) { - int middle = (low + high) >>> 1; - int offset = mIndices[middle]; - - if (DEBUG_SEARCH) { - System.out.println("Comparing string " + (name + ';' + desc) + - " with entry at " + offset + ": " + dumpEntry(offset)); - } - - // Check class number: read short. The byte data is always big endian. - int entryClass = (mData[offset++] & 0xFF) << 8 | (mData[offset++] & 0xFF); - int compare = entryClass - classNumber; - if (compare == 0) { - if (desc != null) { - // Method - int nameLength = name.length(); - compare = compare(mData, offset, (byte) '(', name, nameLength); - if (compare == 0) { - offset += nameLength; - int argsEnd = desc.indexOf(')'); - // Only compare up to the ) -- after that we have a return value in the - // input description, which isn't there in the database - compare = compare(mData, offset, (byte) ')', desc, argsEnd); - if (compare == 0) { - offset += argsEnd + 1; - - if (mData[offset++] == 0) { - // Yes, terminated argument list: get the API level - return UnsignedBytes.toInt(mData[offset]); - } - } - } - } else { - // Field - int nameLength = name.length(); - compare = compare(mData, offset, (byte) 0, name, nameLength); - if (compare == 0) { - offset += nameLength; - if (mData[offset++] == 0) { - // Yes, terminated argument list: get the API level - return UnsignedBytes.toInt(mData[offset]); - } - } - } - } - - if (compare < 0) { - low = middle + 1; - } else if (compare > 0) { - high = middle - 1; - } else { - assert false; // compare == 0 already handled above - return -1; - } - } - - return -1; - } - - /** Clears out any existing lookup instances */ - @VisibleForTesting - static void dispose() { - sInstance.clear(); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiParser.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiParser.java deleted file mode 100644 index b3c2f2a..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ApiParser.java +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2012 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 org.xml.sax.Attributes; -import org.xml.sax.SAXException; -import org.xml.sax.helpers.DefaultHandler; - -import java.util.HashMap; -import java.util.Map; - -/** - * Parser for the simplified XML API format version 1. - */ -public class ApiParser extends DefaultHandler { - - private static final String NODE_API = "api"; - private static final String NODE_CLASS = "class"; - private static final String NODE_FIELD = "field"; - private static final String NODE_METHOD = "method"; - private static final String NODE_EXTENDS = "extends"; - private static final String NODE_IMPLEMENTS = "implements"; - - private static final String ATTR_NAME = "name"; - private static final String ATTR_SINCE = "since"; - - private final Map<String, ApiClass> mClasses = new HashMap<String, ApiClass>(); - - private ApiClass mCurrentClass; - - ApiParser() { - } - - Map<String, ApiClass> getClasses() { - return mClasses; - } - - @Override - public void startElement(String uri, String localName, String qName, Attributes attributes) - throws SAXException { - - if (localName == null || localName.isEmpty()) { - localName = qName; - } - - try { - if (NODE_API.equals(localName)) { - // do nothing. - - } else if (NODE_CLASS.equals(localName)) { - String name = attributes.getValue(ATTR_NAME); - int since = Integer.parseInt(attributes.getValue(ATTR_SINCE)); - - mCurrentClass = addClass(name, since); - - } else if (NODE_EXTENDS.equals(localName)) { - String name = attributes.getValue(ATTR_NAME); - int since = getSince(attributes); - - mCurrentClass.addSuperClass(name, since); - - } else if (NODE_IMPLEMENTS.equals(localName)) { - String name = attributes.getValue(ATTR_NAME); - int since = getSince(attributes); - - mCurrentClass.addInterface(name, since); - - } else if (NODE_METHOD.equals(localName)) { - String name = attributes.getValue(ATTR_NAME); - int since = getSince(attributes); - - mCurrentClass.addMethod(name, since); - - } else if (NODE_FIELD.equals(localName)) { - String name = attributes.getValue(ATTR_NAME); - int since = getSince(attributes); - - mCurrentClass.addField(name, since); - - } - - } finally { - super.startElement(uri, localName, qName, attributes); - } - } - - private ApiClass addClass(String name, int apiLevel) { - ApiClass theClass = mClasses.get(name); - if (theClass == null) { - theClass = new ApiClass(name, apiLevel); - mClasses.put(name, theClass); - } - - return theClass; - } - - private int getSince(Attributes attributes) { - int since = mCurrentClass.getSince(); - String sinceAttr = attributes.getValue(ATTR_SINCE); - - if (sinceAttr != null) { - since = Integer.parseInt(sinceAttr); - } - - return since; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ArraySizeDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ArraySizeDetector.java deleted file mode 100644 index 8bbcffb..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ArraySizeDetector.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * 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.checks; - - -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.TAG_ARRAY; -import static com.android.SdkConstants.TAG_INTEGER_ARRAY; -import static com.android.SdkConstants.TAG_STRING_ARRAY; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -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.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import com.android.utils.Pair; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Checks for arrays with inconsistent item counts - */ -public class ArraySizeDetector extends ResourceXmlDetector { - - /** Are there differences in how many array elements are declared? */ - public static final Issue INCONSISTENT = Issue.create( - "InconsistentArrays", //$NON-NLS-1$ - "Checks for inconsistencies in the number of elements in arrays", - "When an array is translated in a different locale, it should normally have " + - "the same number of elements as the original array. When adding or removing " + - "elements to an array, it is easy to forget to update all the locales, and this " + - "lint warning finds inconsistencies like these.\n" + - "\n" + - "Note however that there may be cases where you really want to declare a " + - "different number of array items in each configuration (for example where " + - "the array represents available options, and those options differ for " + - "different layout orientations and so on), so use your own judgement to " + - "decide if this is really an error.\n" + - "\n" + - "You can suppress this error type if it finds false errors in your project.", - Category.CORRECTNESS, - 7, - Severity.WARNING, - ArraySizeDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - private Multimap<File, Pair<String, Integer>> mFileToArrayCount; - - /** Locations for each array name. Populated during phase 2, if necessary */ - private Map<String, Location> mLocations; - - /** Error messages for each array name. Populated during phase 2, if necessary */ - private Map<String, String> mDescriptions; - - /** Constructs a new {@link ArraySizeDetector} */ - public ArraySizeDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_ARRAY, - TAG_STRING_ARRAY, - TAG_INTEGER_ARRAY - ); - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mFileToArrayCount = ArrayListMultimap.create(30, 5); - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - // Check that all arrays for the same name have the same number of translations - - Set<String> alreadyReported = new HashSet<String>(); - Map<String, Integer> countMap = new HashMap<String, Integer>(); - Map<String, File> fileMap = new HashMap<String, File>(); - - // Process the file in sorted file order to ensure stable output - List<File> keys = new ArrayList<File>(mFileToArrayCount.keySet()); - Collections.sort(keys); - - for (File file : keys) { - Collection<Pair<String, Integer>> pairs = mFileToArrayCount.get(file); - for (Pair<String, Integer> pair : pairs) { - String name = pair.getFirst(); - - if (alreadyReported.contains(name)) { - continue; - } - Integer count = pair.getSecond(); - - Integer current = countMap.get(name); - if (current == null) { - countMap.put(name, count); - fileMap.put(name, file); - } else if (!count.equals(current)) { - alreadyReported.add(name); - - if (mLocations == null) { - mLocations = new HashMap<String, Location>(); - mDescriptions = new HashMap<String, String>(); - } - mLocations.put(name, null); - - String thisName = file.getParentFile().getName() + File.separator - + file.getName(); - File otherFile = fileMap.get(name); - String otherName = otherFile.getParentFile().getName() + File.separator - + otherFile.getName(); - String message = String.format( - "Array %1$s has an inconsistent number of items (%2$d in %3$s, %4$d in %5$s)", - name, count, thisName, current, otherName); - mDescriptions.put(name, message); - } - } - } - - if (mLocations != null) { - // Request another scan through the resources such that we can - // gather the actual locations - context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); - } - mFileToArrayCount = null; - } else { - if (mLocations != null) { - List<String> names = new ArrayList<String>(mLocations.keySet()); - Collections.sort(names); - for (String name : names) { - Location location = mLocations.get(name); - // We were prepending locations, but we want to prefer the base folders - location = Location.reverse(location); - - // Make sure we still have a conflict, in case one or more of the - // elements were marked with tools:ignore - int count = -1; - LintDriver driver = context.getDriver(); - boolean foundConflict = false; - Location curr; - for (curr = location; curr != null; curr = curr.getSecondary()) { - Object clientData = curr.getClientData(); - if (clientData instanceof Node) { - Node node = (Node) clientData; - if (driver.isSuppressed(INCONSISTENT, node)) { - continue; - } - int newCount = LintUtils.getChildCount(node); - if (newCount != count) { - if (count == -1) { - count = newCount; // first number encountered - } else { - foundConflict = true; - break; - } - } - } else { - foundConflict = true; - break; - } - } - - // Through one or more tools:ignore, there is no more conflict so - // ignore this element - if (!foundConflict) { - continue; - } - - String message = mDescriptions.get(name); - context.report(INCONSISTENT, location, message, null); - } - } - - mLocations = null; - mDescriptions = null; - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - int phase = context.getPhase(); - - Attr attribute = element.getAttributeNode(ATTR_NAME); - if (attribute == null || attribute.getValue().isEmpty()) { - if (phase != 1) { - return; - } - context.report(INCONSISTENT, element, context.getLocation(element), - String.format("Missing name attribute in %1$s declaration", element.getTagName()), - null); - } else { - String name = attribute.getValue(); - if (phase == 1) { - if (context.getProject().getReportIssues()) { - int childCount = LintUtils.getChildCount(element); - mFileToArrayCount.put(context.file, Pair.of(name, childCount)); - } - } else { - assert phase == 2; - if (mLocations.containsKey(name)) { - if (context.getDriver().isSuppressed(INCONSISTENT, element)) { - return; - } - Location location = context.getLocation(element); - location.setClientData(element); - location.setMessage(String.format("Declaration with array size (%1$d)", - LintUtils.getChildCount(element))); - location.setSecondary(mLocations.get(name)); - mLocations.put(name, location); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java deleted file mode 100644 index 1ebb701..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/BuiltinIssueRegistry.java +++ /dev/null @@ -1,382 +0,0 @@ -/* - * 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.checks; - -import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled; -import static com.android.tools.lint.detector.api.LintUtils.endsWith; - -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.prefs.AndroidLocation; -import com.android.prefs.AndroidLocation.AndroidLocationException; -import com.android.tools.lint.client.api.IssueRegistry; -import com.android.tools.lint.detector.api.Issue; -import com.google.common.annotations.Beta; - -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; - -/** Registry which provides a list of checks to be performed on an Android project */ -public class BuiltinIssueRegistry extends IssueRegistry { - /** Folder name in the .android dir where additional detector jars are found */ - private static final String LINT_FOLDER = "lint"; //$NON-NLS-1$ - - /** - * Manifest constant for declaring an issue provider. Example: - * Lint-Registry: foo.bar.CustomIssueRegistry - */ - private static final String MF_LINT_REGISTRY = "Lint-Registry"; //$NON-NLS-1$ - - private static final List<Issue> sIssues; - - static { - final int initialCapacity = 141; - List<Issue> issues = new ArrayList<Issue>(initialCapacity); - - issues.add(AccessibilityDetector.ISSUE); - issues.add(LabelForDetector.ISSUE); - issues.add(MathDetector.ISSUE); - issues.add(FieldGetterDetector.ISSUE); - issues.add(SdCardDetector.ISSUE); - issues.add(ApiDetector.UNSUPPORTED); - issues.add(ApiDetector.INLINED); - issues.add(ApiDetector.OVERRIDE); - issues.add(InvalidPackageDetector.ISSUE); - issues.add(DuplicateIdDetector.CROSS_LAYOUT); - issues.add(DuplicateIdDetector.WITHIN_LAYOUT); - issues.add(DuplicateResourceDetector.ISSUE); - issues.add(WrongIdDetector.UNKNOWN_ID); - issues.add(WrongIdDetector.UNKNOWN_ID_LAYOUT); - issues.add(StateListDetector.ISSUE); - issues.add(StyleCycleDetector.ISSUE); - issues.add(InefficientWeightDetector.INEFFICIENT_WEIGHT); - issues.add(InefficientWeightDetector.NESTED_WEIGHTS); - issues.add(InefficientWeightDetector.BASELINE_WEIGHTS); - issues.add(InefficientWeightDetector.WRONG_0DP); - issues.add(InefficientWeightDetector.ORIENTATION); - issues.add(ScrollViewChildDetector.ISSUE); - issues.add(DeprecationDetector.ISSUE); - issues.add(ObsoleteLayoutParamsDetector.ISSUE); - issues.add(MergeRootFrameLayoutDetector.ISSUE); - issues.add(NestedScrollingWidgetDetector.ISSUE); - issues.add(ChildCountDetector.SCROLLVIEW_ISSUE); - issues.add(ChildCountDetector.ADAPTERVIEW_ISSUE); - issues.add(UseCompoundDrawableDetector.ISSUE); - issues.add(UselessViewDetector.USELESS_PARENT); - issues.add(UselessViewDetector.USELESS_LEAF); - issues.add(TooManyViewsDetector.TOO_MANY); - issues.add(TooManyViewsDetector.TOO_DEEP); - issues.add(GridLayoutDetector.ISSUE); - issues.add(OverrideDetector.ISSUE); - issues.add(OnClickDetector.ISSUE); - issues.add(ViewTagDetector.ISSUE); - issues.add(LocaleDetector.STRING_LOCALE); - issues.add(LocaleDetector.DATE_FORMAT); - issues.add(RegistrationDetector.ISSUE); - issues.add(MissingClassDetector.MISSING); - issues.add(MissingClassDetector.INSTANTIATABLE); - issues.add(MissingClassDetector.INNERCLASS); - issues.add(MissingIdDetector.ISSUE); - issues.add(WrongCaseDetector.WRONGCASE); - issues.add(HandlerDetector.ISSUE); - issues.add(FragmentDetector.ISSUE); - issues.add(TranslationDetector.EXTRA); - issues.add(TranslationDetector.MISSING); - issues.add(HardcodedValuesDetector.ISSUE); - issues.add(Utf8Detector.ISSUE); - issues.add(DosLineEndingDetector.ISSUE); - issues.add(CommentDetector.EASTEREGG); - issues.add(CommentDetector.STOPSHIP); - issues.add(ProguardDetector.WRONGKEEP); - issues.add(ProguardDetector.SPLITCONFIG); - issues.add(PxUsageDetector.PX_ISSUE); - issues.add(PxUsageDetector.DP_ISSUE); - issues.add(PxUsageDetector.IN_MM_ISSUE); - issues.add(PxUsageDetector.SMALL_SP_ISSUE); - issues.add(TextFieldDetector.ISSUE); - issues.add(TextViewDetector.ISSUE); - issues.add(TextViewDetector.SELECTABLE); - issues.add(UnusedResourceDetector.ISSUE); - issues.add(UnusedResourceDetector.ISSUE_IDS); - issues.add(ExtraTextDetector.ISSUE); - issues.add(PrivateResourceDetector.ISSUE); - issues.add(ArraySizeDetector.INCONSISTENT); - issues.add(HardcodedDebugModeDetector.ISSUE); - issues.add(ManifestOrderDetector.ORDER); - issues.add(ManifestOrderDetector.USES_SDK); - issues.add(ManifestOrderDetector.MULTIPLE_USES_SDK); - issues.add(ManifestOrderDetector.WRONG_PARENT); - issues.add(ManifestOrderDetector.DUPLICATE_ACTIVITY); - issues.add(ManifestOrderDetector.TARGET_NEWER); - issues.add(ManifestOrderDetector.ALLOW_BACKUP); - issues.add(ManifestOrderDetector.UNIQUE_PERMISSION); - issues.add(ManifestOrderDetector.SET_VERSION); - issues.add(ManifestOrderDetector.ILLEGAL_REFERENCE); - issues.add(ManifestTypoDetector.ISSUE); - issues.add(SecurityDetector.EXPORTED_PROVIDER); - issues.add(SecurityDetector.EXPORTED_SERVICE); - issues.add(SecurityDetector.EXPORTED_RECEIVER); - issues.add(SecurityDetector.OPEN_PROVIDER); - issues.add(SecurityDetector.WORLD_READABLE); - issues.add(SecurityDetector.WORLD_WRITEABLE); - issues.add(SecureRandomDetector.ISSUE); - issues.add(IconDetector.GIF_USAGE); - issues.add(IconDetector.ICON_DENSITIES); - issues.add(IconDetector.ICON_MISSING_FOLDER); - issues.add(IconDetector.ICON_DIP_SIZE); - issues.add(IconDetector.ICON_EXPECTED_SIZE); - issues.add(IconDetector.ICON_LOCATION); - issues.add(IconDetector.DUPLICATES_NAMES); - issues.add(IconDetector.DUPLICATES_CONFIGURATIONS); - issues.add(IconDetector.ICON_NODPI); - issues.add(IconDetector.ICON_EXTENSION); - issues.add(IconDetector.ICON_COLORS); - issues.add(IconDetector.ICON_XML_AND_PNG); - issues.add(IconDetector.ICON_LAUNCHER_SHAPE); - issues.add(TypographyDetector.DASHES); - issues.add(TypographyDetector.QUOTES); - issues.add(TypographyDetector.FRACTIONS); - issues.add(TypographyDetector.ELLIPSIS); - issues.add(TypographyDetector.OTHER); - issues.add(ButtonDetector.ORDER); - issues.add(ButtonDetector.CASE); - issues.add(ButtonDetector.BACKBUTTON); - issues.add(ButtonDetector.STYLE); - issues.add(DetectMissingPrefix.MISSING_NAMESPACE); - issues.add(OverdrawDetector.ISSUE); - issues.add(StringFormatDetector.INVALID); - issues.add(StringFormatDetector.ARG_COUNT); - issues.add(StringFormatDetector.ARG_TYPES); - issues.add(TypoDetector.ISSUE); - issues.add(ViewTypeDetector.ISSUE); - issues.add(WrongImportDetector.ISSUE); - issues.add(WrongLocationDetector.ISSUE); - issues.add(ViewConstructorDetector.ISSUE); - issues.add(NamespaceDetector.CUSTOMVIEW); - issues.add(NamespaceDetector.UNUSED); - issues.add(NamespaceDetector.TYPO); - issues.add(AlwaysShowActionDetector.ISSUE); - issues.add(TitleDetector.ISSUE); - issues.add(ColorUsageDetector.ISSUE); - issues.add(JavaPerformanceDetector.PAINT_ALLOC); - issues.add(JavaPerformanceDetector.USE_VALUEOF); - issues.add(JavaPerformanceDetector.USE_SPARSEARRAY); - issues.add(WakelockDetector.ISSUE); - issues.add(CleanupDetector.RECYCLE_RESOURCE); - issues.add(CleanupDetector.COMMIT_FRAGMENT); - issues.add(SetJavaScriptEnabledDetector.ISSUE); - issues.add(ToastDetector.ISSUE); - issues.add(SharedPrefsDetector.ISSUE); - issues.add(CutPasteDetector.ISSUE); - issues.add(NonInternationalizedSmsDetector.ISSUE); - issues.add(PrivateKeyDetector.ISSUE); - issues.add(AnnotationDetector.ISSUE); - issues.add(SystemPermissionsDetector.ISSUE); - issues.add(RequiredAttributeDetector.ISSUE); - issues.add(WrongCallDetector.ISSUE); - - assert initialCapacity >= issues.size() : issues.size(); - - addCustomIssues(issues); - - sIssues = Collections.unmodifiableList(issues); - - // Check that ids are unique - if (assertionsEnabled()) { - Set<String> ids = new HashSet<String>(); - for (Issue issue : sIssues) { - String id = issue.getId(); - assert !ids.contains(id) : "Duplicate id " + id; //$NON-NLS-1$ - ids.add(id); - } - } - } - - /** - * Constructs a new {@link BuiltinIssueRegistry} - */ - public BuiltinIssueRegistry() { - } - - @NonNull - @Override - public List<Issue> getIssues() { - return sIssues; - } - - /** - * Add in custom issues registered by the user - via an environment variable - * or in the .android/lint directory. - */ - private static void addCustomIssues(List<Issue> issues) { - // Look for additional detectors registered by the user, via - // (1) an environment variable (useful for build servers etc), and - // (2) via jar files in the .android/lint directory - Set<File> files = null; - try { - File lint = new File(AndroidLocation.getFolder() + File.separator + LINT_FOLDER); - if (lint.exists()) { - File[] list = lint.listFiles(); - if (list != null) { - for (File jarFile : list) { - if (endsWith(jarFile.getName(), ".jar")) { //$NON-NLS-1$ - if (files == null) { - files = new HashSet<File>(); - } - files.add(jarFile); - addIssuesFromJar(jarFile, issues); - } - } - } - } - } catch (AndroidLocationException e) { - // Ignore -- no android dir, so no rules to load. - } - - String lintClassPath = System.getenv("ANDROID_LINT_JARS"); //$NON-NLS-1$ - if (lintClassPath != null && !lintClassPath.isEmpty()) { - String[] paths = lintClassPath.split(File.pathSeparator); - for (String path : paths) { - File jarFile = new File(path); - if (jarFile.exists() && (files == null || !files.contains(jarFile))) { - addIssuesFromJar(jarFile, issues); - } - } - } - } - - /** Add the issues found in the given jar file into the given list of issues */ - private static void addIssuesFromJar(File jarFile, List<Issue> issues) { - JarFile jarfile = null; - try { - jarfile = new JarFile(jarFile); - Manifest manifest = jarfile.getManifest(); - Attributes attrs = manifest.getMainAttributes(); - Object object = attrs.get(new Attributes.Name(MF_LINT_REGISTRY)); - if (object instanceof String) { - String className = (String) object; - - // Make a class loader for this jar - try { - URL url = jarFile.toURI().toURL(); - URLClassLoader loader = new URLClassLoader(new URL[] { url }, - BuiltinIssueRegistry.class.getClassLoader()); - try { - Class<?> registryClass = Class.forName(className, true, loader); - IssueRegistry registry = (IssueRegistry) registryClass.newInstance(); - for (Issue issue : registry.getIssues()) { - issues.add(issue); - } - } catch (Throwable e) { - log(e); - } - } catch (MalformedURLException e) { - log(e); - } - } - } catch (IOException e) { - log(e); - } finally { - if (jarfile != null) { - try { - jarfile.close(); - } catch (IOException e) { - // Nothing to be done - } - } - } - } - - private static void log(Throwable e) { - // TODO: Where do we log this? There's no embedding tool context here. For now, - // just dump to the console so detector developers get some feedback on what went - // wrong. - e.printStackTrace(); - } - - private static Set<Issue> sAdtFixes; - - /** - * 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 - */ - @Beta - public boolean hasAutoFix(String tool, Issue issue) { - assert tool.equals("adt"); // This is not yet a generic facility; - // the primary purpose right now is to allow for example the HTML report - // to give a hint to the user that some fixes don't require manual work - - return getIssuesWithFixes().contains(issue); - } - - private static Set<Issue> getIssuesWithFixes() { - if (sAdtFixes == null) { - sAdtFixes = new HashSet<Issue>(25); - sAdtFixes.add(InefficientWeightDetector.INEFFICIENT_WEIGHT); - sAdtFixes.add(AccessibilityDetector.ISSUE); - sAdtFixes.add(InefficientWeightDetector.BASELINE_WEIGHTS); - sAdtFixes.add(HardcodedValuesDetector.ISSUE); - sAdtFixes.add(UselessViewDetector.USELESS_LEAF); - sAdtFixes.add(UselessViewDetector.USELESS_PARENT); - sAdtFixes.add(PxUsageDetector.PX_ISSUE); - sAdtFixes.add(TextFieldDetector.ISSUE); - sAdtFixes.add(SecurityDetector.EXPORTED_SERVICE); - sAdtFixes.add(DetectMissingPrefix.MISSING_NAMESPACE); - sAdtFixes.add(ScrollViewChildDetector.ISSUE); - sAdtFixes.add(ObsoleteLayoutParamsDetector.ISSUE); - sAdtFixes.add(TypographyDetector.DASHES); - sAdtFixes.add(TypographyDetector.ELLIPSIS); - sAdtFixes.add(TypographyDetector.FRACTIONS); - sAdtFixes.add(TypographyDetector.OTHER); - sAdtFixes.add(TypographyDetector.QUOTES); - sAdtFixes.add(UseCompoundDrawableDetector.ISSUE); - sAdtFixes.add(ApiDetector.UNSUPPORTED); - sAdtFixes.add(TypoDetector.ISSUE); - sAdtFixes.add(ManifestOrderDetector.ALLOW_BACKUP); - sAdtFixes.add(MissingIdDetector.ISSUE); - sAdtFixes.add(TranslationDetector.MISSING); - sAdtFixes.add(DosLineEndingDetector.ISSUE); - } - - return sAdtFixes; - } - - /** - * Reset the registry such that it recomputes its available issues. - * <p> - * NOTE: This is only intended for testing purposes. - */ - @VisibleForTesting - public static void reset() { - IssueRegistry.reset(); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ButtonDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ButtonDetector.java deleted file mode 100644 index e756e03..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ButtonDetector.java +++ /dev/null @@ -1,782 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_STRING_PREFIX; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BACKGROUND; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF; -import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_ORIENTATION; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.ATTR_TEXT; -import static com.android.SdkConstants.BUTTON; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.RELATIVE_LAYOUT; -import static com.android.SdkConstants.STRING_PREFIX; -import static com.android.SdkConstants.TABLE_ROW; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.VALUE_SELECTABLE_ITEM_BACKGROUND; -import static com.android.SdkConstants.VALUE_TRUE; -import static com.android.SdkConstants.VALUE_VERTICAL; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -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.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Check which looks at the order of buttons in dialogs and makes sure that - * "the dismissive action of a dialog is always on the left whereas the affirmative actions - * are on the right." - * <p> - * This only looks for the affirmative and dismissive actions named "OK" and "Cancel"; - * "Cancel" usually works, but the affirmative action often has many other names -- "Done", - * "Send", "Go", etc. - * <p> - * TODO: Perhaps we should look for Yes/No dialogs and suggested they be rephrased as - * Cancel/OK dialogs? Similarly, consider "Abort" a synonym for "Cancel" ? - */ -public class ButtonDetector extends ResourceXmlDetector { - /** Name of cancel value ("Cancel") */ - private static final String CANCEL_LABEL = "Cancel"; - /** Name of OK value ("Cancel") */ - private static final String OK_LABEL = "OK"; - /** Name of Back value ("Back") */ - private static final String BACK_LABEL = "Back"; - - /** Layout text attribute reference to {@code @android:string/ok} */ - private static final String ANDROID_OK_RESOURCE = - ANDROID_STRING_PREFIX + "ok"; //$NON-NLS-1$ - /** Layout text attribute reference to {@code @android:string/cancel} */ - private static final String ANDROID_CANCEL_RESOURCE = - ANDROID_STRING_PREFIX + "cancel"; //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue ORDER = Issue.create( - "ButtonOrder", //$NON-NLS-1$ - "Ensures the dismissive action of a dialog is on the left and affirmative on " + - "the right", - - "According to the Android Design Guide,\n" + - "\n" + - "\"Action buttons are typically Cancel and/or OK, with OK indicating the preferred " + - "or most likely action. However, if the options consist of specific actions such " + - "as Close or Wait rather than a confirmation or cancellation of the action " + - "described in the content, then all the buttons should be active verbs. As a rule, " + - "the dismissive action of a dialog is always on the left whereas the affirmative " + - "actions are on the right.\"\n" + - "\n" + - "This check looks for button bars and buttons which look like cancel buttons, " + - "and makes sure that these are on the left.", - - Category.USABILITY, - 8, - Severity.WARNING, - ButtonDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setMoreInfo( - "http://developer.android.com/design/building-blocks/dialogs.html"); //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue STYLE = Issue.create( - "ButtonStyle", //$NON-NLS-1$ - "Ensures that buttons in button bars are borderless", - - "Button bars typically use a borderless style for the buttons. Set the " + - "`style=\"?android:attr/buttonBarButtonStyle\"` attribute " + - "on each of the buttons, and set `style=\"?android:attr/buttonBarStyle\"` on " + - "the parent layout", - - Category.USABILITY, - 5, - Severity.WARNING, - ButtonDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setMoreInfo( - "http://developer.android.com/design/building-blocks/buttons.html"); //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue BACKBUTTON = Issue.create( - "BackButton", //$NON-NLS-1$ - "Looks for Back buttons, which are not common on the Android platform.", - // TODO: Look for ">" as label suffixes as well - - "According to the Android Design Guide,\n" + - "\n" + - "\"Other platforms use an explicit back button with label to allow the user " + - "to navigate up the application's hierarchy. Instead, Android uses the main " + - "action bar's app icon for hierarchical navigation and the navigation bar's " + - "back button for temporal navigation.\"" + - "\n" + - "This check is not very sophisticated (it just looks for buttons with the " + - "label \"Back\"), so it is disabled by default to not trigger on common " + - "scenarios like pairs of Back/Next buttons to paginate through screens.", - - Category.USABILITY, - 6, - Severity.WARNING, - ButtonDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setEnabledByDefault(false) - .setMoreInfo( - "http://developer.android.com/design/patterns/pure-android.html"); //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue CASE = Issue.create( - "ButtonCase", //$NON-NLS-1$ - "Ensures that Cancel/OK dialog buttons use the canonical capitalization", - - "The standard capitalization for OK/Cancel dialogs is \"OK\" and \"Cancel\". " + - "To ensure that your dialogs use the standard strings, you can use " + - "the resource strings @android:string/ok and @android:string/cancel.", - - Category.USABILITY, - 2, - Severity.WARNING, - ButtonDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Set of resource names whose value was either OK or Cancel */ - private Set<String> mApplicableResources; - - /** - * Map of resource names we'd like resolved into strings in phase 2. The - * values should be filled in with the actual string contents. - */ - private Map<String, String> mKeyToLabel; - - /** - * Set of elements we've already warned about. If we've already complained - * about a cancel button, don't also report the OK button (since it's listed - * for the warnings on OK buttons). - */ - private Set<Element> mIgnore; - - /** Constructs a new {@link ButtonDetector} */ - public ButtonDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList(BUTTON, TAG_STRING); - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - int phase = context.getPhase(); - if (phase == 1 && mApplicableResources != null) { - // We found resources for the string "Cancel"; perform a second pass - // where we check layout text attributes against these strings. - context.getDriver().requestRepeat(this, Scope.RESOURCE_FILE_SCOPE); - } - } - - private static String stripLabel(String text) { - text = text.trim(); - if (text.length() > 2 - && (text.charAt(0) == '"' || text.charAt(0) == '\'') - && (text.charAt(0) == text.charAt(text.length() - 1))) { - text = text.substring(1, text.length() - 1); - } - - return text; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - // This detector works in two passes. - // In pass 1, it looks in layout files for hardcoded strings of "Cancel", or - // references to @string/cancel or @android:string/cancel. - // It also looks in values/ files for strings whose value is "Cancel", - // and if found, stores the corresponding keys in a map. (This is necessary - // since value files are processed after layout files). - // Then, if at the end of phase 1 any "Cancel" string resources were - // found in the value files, then it requests a *second* phase, - // where it looks only for <Button>'s whose text matches one of the - // cancel string resources. - int phase = context.getPhase(); - String tagName = element.getTagName(); - if (phase == 1 && tagName.equals(TAG_STRING)) { - NodeList childNodes = element.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - String text = child.getNodeValue(); - for (int j = 0, len = text.length(); j < len; j++) { - char c = text.charAt(j); - if (!Character.isWhitespace(c)) { - if (c == '"' || c == '\'') { - continue; - } - if (LintUtils.startsWith(text, CANCEL_LABEL, j)) { - String label = stripLabel(text); - if (label.equalsIgnoreCase(CANCEL_LABEL)) { - String name = element.getAttribute(ATTR_NAME); - foundResource(context, name, element); - - if (!label.equals(CANCEL_LABEL) - && isEnglishResource(context) - && context.isEnabled(CASE)) { - assert label.equalsIgnoreCase(CANCEL_LABEL); - context.report(CASE, child, context.getLocation(child), - String.format( - "The standard Android way to capitalize %1$s " + - "is \"Cancel\" (tip: use @android:string/ok instead)", - label), null); - } - } - } else if (LintUtils.startsWith(text, OK_LABEL, j)) { - String label = stripLabel(text); - if (label.equalsIgnoreCase(OK_LABEL)) { - String name = element.getAttribute(ATTR_NAME); - foundResource(context, name, element); - - if (!label.equals(OK_LABEL) - && isEnglishResource(context) - && context.isEnabled(CASE)) { - assert text.trim().equalsIgnoreCase(OK_LABEL); - context.report(CASE, child, context.getLocation(child), - String.format( - "The standard Android way to capitalize %1$s " + - "is \"OK\" (tip: use @android:string/ok instead)", - label), null); - } - } - } else if (LintUtils.startsWith(text, BACK_LABEL, j) && - stripLabel(text).equalsIgnoreCase(BACK_LABEL)) { - String name = element.getAttribute(ATTR_NAME); - foundResource(context, name, element); - } - break; - } - } - } - } - } else if (tagName.equals(BUTTON)) { - if (phase == 1) { - if (isInButtonBar(element) - && !element.hasAttribute(ATTR_STYLE) - && !VALUE_SELECTABLE_ITEM_BACKGROUND.equals( - element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) - && (context.getProject().getMinSdk() >= 11 - || context.getFolderVersion() >= 11) - && context.isEnabled(STYLE) - && !parentDefinesSelectableItem(element)) { - context.report(STYLE, element, context.getLocation(element), - "Buttons in button bars should be borderless; use " + - "style=\"?android:attr/buttonBarButtonStyle\" (and " + - "?android:attr/buttonBarStyle on the parent)", - null); - } - } - - String text = element.getAttributeNS(ANDROID_URI, ATTR_TEXT); - if (phase == 2) { - if (mApplicableResources.contains(text)) { - String key = text; - if (key.startsWith(STRING_PREFIX)) { - key = key.substring(STRING_PREFIX.length()); - } - String label = mKeyToLabel.get(key); - boolean isCancel = CANCEL_LABEL.equalsIgnoreCase(label); - if (isCancel) { - if (isWrongCancelPosition(element)) { - reportCancelPosition(context, element); - } - } else if (OK_LABEL.equalsIgnoreCase(label)) { - if (isWrongOkPosition(element)) { - reportOkPosition(context, element); - } - } else { - assert BACK_LABEL.equalsIgnoreCase(label) : label + ':' + context.file; - Location location = context.getLocation(element); - if (context.isEnabled(BACKBUTTON)) { - context.report(BACKBUTTON, element, location, - "Back buttons are not standard on Android; see design guide's " + - "navigation section", null); - } - } - } - } else if (text.equals(CANCEL_LABEL) || text.equals(ANDROID_CANCEL_RESOURCE)) { - if (isWrongCancelPosition(element)) { - reportCancelPosition(context, element); - } - } else if (text.equals(OK_LABEL) || text.equals(ANDROID_OK_RESOURCE)) { - if (isWrongOkPosition(element)) { - reportOkPosition(context, element); - } - } - } - } - - private static boolean parentDefinesSelectableItem(Element element) { - String background = element.getAttributeNS(ANDROID_URI, ATTR_BACKGROUND); - if (VALUE_SELECTABLE_ITEM_BACKGROUND.equals(background)) { - return true; - } - - Node parent = element.getParentNode(); - if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) { - return parentDefinesSelectableItem((Element) parent); - } - - return false; - } - - /** Report the given OK button as being in the wrong position */ - private void reportOkPosition(XmlContext context, Element element) { - report(context, element, false /*isCancel*/); - } - - /** Report the given Cancel button as being in the wrong position */ - private void reportCancelPosition(XmlContext context, Element element) { - report(context, element, true /*isCancel*/); - } - - - /** The Ok/Cancel detector only works with default and English locales currently. - * TODO: Add in patterns for other languages. We can use the - * @android:string/ok and @android:string/cancel localizations to look - * up the canonical ones. */ - private static boolean isEnglishResource(XmlContext context) { - String folder = context.file.getParentFile().getName(); - if (folder.indexOf('-') != -1) { - String[] qualifiers = folder.split("-"); //$NON-NLS-1$ - for (String qualifier : qualifiers) { - if (qualifier.equals("en")) { //$NON-NLS-1$ - return true; - } - } - return false; - } - - // Default folder ("values") - may not be English but we'll consider matches - // on "OK", "Cancel" and "Back" as matches there - return true; - } - - /** - * We've found a resource reference to some label we're interested in ("OK", - * "Cancel", "Back", ...). Record the corresponding name such that in the - * next pass through the layouts we can check the context (for OK/Cancel the - * button order etc). - */ - private void foundResource(XmlContext context, String name, Element element) { - if (!isEnglishResource(context)) { - return; - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - if (mApplicableResources == null) { - mApplicableResources = new HashSet<String>(); - } - - mApplicableResources.add(STRING_PREFIX + name); - - // ALSO record all the other string resources in this file to pick up other - // labels. If you define "OK" in one resource file and "Cancel" in another - // this won't work, but that's probably not common and has lower overhead. - Node parentNode = element.getParentNode(); - - List<Element> items = LintUtils.getChildren(parentNode); - if (mKeyToLabel == null) { - mKeyToLabel = new HashMap<String, String>(items.size()); - } - for (Element item : items) { - String itemName = item.getAttribute(ATTR_NAME); - NodeList childNodes = item.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - String text = stripLabel(child.getNodeValue()); - if (!text.isEmpty()) { - mKeyToLabel.put(itemName, text); - break; - } - } - } - } - } - - /** Report the given OK/Cancel button as being in the wrong position */ - private void report(XmlContext context, Element element, boolean isCancel) { - if (!context.isEnabled(ORDER)) { - return; - } - - if (mIgnore != null && mIgnore.contains(element)) { - return; - } - - int target = context.getProject().getTargetSdk(); - if (target < 14) { - // If you're only targeting pre-ICS UI's, this is not an issue - return; - } - - boolean mustCreateIcsLayout = false; - if (context.getProject().getMinSdk() < 14) { - // If you're *also* targeting pre-ICS UIs, then this reverse button - // order is correct for layouts intended for pre-ICS and incorrect for - // ICS layouts. - // - // Therefore, we need to know if this layout is an ICS layout or - // a pre-ICS layout. - boolean isIcsLayout = context.getFolderVersion() >= 14; - if (!isIcsLayout) { - // This layout is not an ICS layout. However, there *must* also be - // an ICS layout here, or this button order will be wrong: - File res = context.file.getParentFile().getParentFile(); - File[] resFolders = res.listFiles(); - String fileName = context.file.getName(); - if (resFolders != null) { - for (File folder : resFolders) { - String folderName = folder.getName(); - if (folderName.startsWith(SdkConstants.FD_RES_LAYOUT) - && folderName.contains("-v14")) { //$NON-NLS-1$ - File layout = new File(folder, fileName); - if (layout.exists()) { - // Yes, a v14 specific layout is available so this pre-ICS - // layout order is not a problem - return; - } - } - } - } - mustCreateIcsLayout = true; - } - } - - List<Element> buttons = LintUtils.getChildren(element.getParentNode()); - - if (mIgnore == null) { - mIgnore = new HashSet<Element>(); - } - for (Element button : buttons) { - // Mark all the siblings in the ignore list to ensure that we don't - // report *both* the Cancel and the OK button in "OK | Cancel" - mIgnore.add(button); - } - - String message; - if (isCancel) { - message = "Cancel button should be on the left"; - } else { - message = "OK button should be on the right"; - } - - if (mustCreateIcsLayout) { - message = String.format( - "Layout uses the wrong button order for API >= 14: Create a " + - "layout-v14/%1$s file with opposite order: %2$s", - context.file.getName(), message); - } - - // Show existing button order? We can only do that for LinearLayouts - // since in for example a RelativeLayout the order of the elements may - // not be the same as the visual order - String layout = element.getParentNode().getNodeName(); - if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) { - List<String> labelList = getLabelList(buttons); - String wrong = describeButtons(labelList); - sortButtons(labelList); - String right = describeButtons(labelList); - message += String.format(" (was \"%1$s\", should be \"%2$s\")", wrong, right); - } - - Location location = context.getLocation(element); - context.report(ORDER, element, location, message, null); - } - - /** - * Sort a list of label buttons into the expected order (Cancel on the left, - * OK on the right - */ - private static void sortButtons(List<String> labelList) { - for (int i = 0, n = labelList.size(); i < n; i++) { - String label = labelList.get(i); - if (label.equalsIgnoreCase(CANCEL_LABEL) && i > 0) { - swap(labelList, 0, i); - } else if (label.equalsIgnoreCase(OK_LABEL) && i < n - 1) { - swap(labelList, n - 1, i); - } - } - } - - /** Swaps the strings at positions i and j */ - private static void swap(List<String> strings, int i, int j) { - if (i != j) { - String temp = strings.get(i); - strings.set(i, strings.get(j)); - strings.set(j, temp); - } - } - - /** Creates a display string for a list of button labels, such as "Cancel | OK" */ - private static String describeButtons(List<String> labelList) { - StringBuilder sb = new StringBuilder(80); - for (String label : labelList) { - if (sb.length() > 0) { - sb.append(" | "); //$NON-NLS-1$ - } - sb.append(label); - } - - return sb.toString(); - } - - /** Returns the ordered list of button labels */ - private List<String> getLabelList(List<Element> views) { - List<String> labels = new ArrayList<String>(); - - if (mIgnore == null) { - mIgnore = new HashSet<Element>(); - } - - for (Element view : views) { - if (view.getTagName().equals(BUTTON)) { - String text = view.getAttributeNS(ANDROID_URI, ATTR_TEXT); - String label = getLabel(text); - labels.add(label); - - // Mark all the siblings in the ignore list to ensure that we don't - // report *both* the Cancel and the OK button in "OK | Cancel" - mIgnore.add(view); - } - } - - return labels; - } - - private String getLabel(String key) { - String label = null; - if (key.startsWith(ANDROID_STRING_PREFIX)) { - if (key.equals(ANDROID_OK_RESOURCE)) { - label = OK_LABEL; - } else if (key.equals(ANDROID_CANCEL_RESOURCE)) { - label = CANCEL_LABEL; - } - } else if (mKeyToLabel != null) { - if (key.startsWith(STRING_PREFIX)) { - label = mKeyToLabel.get(key.substring(STRING_PREFIX.length())); - } - } - - if (label == null) { - label = key; - } - - if (label.indexOf(' ') != -1 && label.indexOf('"') == -1) { - label = '"' + label + '"'; - } - - return label; - } - - /** Is the cancel button in the wrong position? It has to be on the left. */ - private boolean isWrongCancelPosition(Element element) { - return isWrongPosition(element, true /*isCancel*/); - } - - /** Is the OK button in the wrong position? It has to be on the right. */ - private boolean isWrongOkPosition(Element element) { - return isWrongPosition(element, false /*isCancel*/); - } - - private static boolean isInButtonBar(Element element) { - assert element.getTagName().equals(BUTTON) : element.getTagName(); - Node parentNode = element.getParentNode(); - if (parentNode.getNodeType() != Node.ELEMENT_NODE) { - return false; - } - Element parent = (Element) parentNode; - - String style = parent.getAttribute(ATTR_STYLE); - if (style != null && style.contains("buttonBarStyle")) { //$NON-NLS-1$ - return true; - } - - // Don't warn about single Cancel / OK buttons - if (LintUtils.getChildCount(parent) < 2) { - return false; - } - - String layout = parent.getTagName(); - if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) { - String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); - if (VALUE_VERTICAL.equals(orientation)) { - return false; - } - } else { - return false; - } - - // Ensure that all the children are buttons - Node n = parent.getFirstChild(); - while (n != null) { - if (n.getNodeType() == Node.ELEMENT_NODE) { - if (!BUTTON.equals(n.getNodeName())) { - return false; - } - } - n = n.getNextSibling(); - } - - return true; - } - - /** Is the given button in the wrong position? */ - private static boolean isWrongPosition(Element element, boolean isCancel) { - Node parentNode = element.getParentNode(); - if (parentNode.getNodeType() != Node.ELEMENT_NODE) { - return false; - } - Element parent = (Element) parentNode; - - // Don't warn about single Cancel / OK buttons - if (LintUtils.getChildCount(parent) < 2) { - return false; - } - - String layout = parent.getTagName(); - if (layout.equals(LINEAR_LAYOUT) || layout.equals(TABLE_ROW)) { - String orientation = parent.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); - if (VALUE_VERTICAL.equals(orientation)) { - return false; - } - - if (isCancel) { - Node n = element.getPreviousSibling(); - while (n != null) { - if (n.getNodeType() == Node.ELEMENT_NODE) { - return true; - } - n = n.getPreviousSibling(); - } - } else { - Node n = element.getNextSibling(); - while (n != null) { - if (n.getNodeType() == Node.ELEMENT_NODE) { - return true; - } - n = n.getNextSibling(); - } - } - - return false; - } else if (layout.equals(RELATIVE_LAYOUT)) { - // In RelativeLayouts, look for attachments which look like a clear sign - // that the OK or Cancel buttons are out of order: - // -- a left attachment on a Cancel button (where the left attachment - // is a button; we don't want to complain if it's pointing to a spacer - // or image or progress indicator etc) - // -- a right-side parent attachment on a Cancel button (unless it's also - // attached on the left, e.g. a cancel button stretching across the - // layout) - // etc. - if (isCancel) { - if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_RIGHT_OF) - && isButtonId(parent, element.getAttributeNS(ANDROID_URI, - ATTR_LAYOUT_TO_RIGHT_OF))) { - return true; - } - if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT) && - !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT)) { - return true; - } - } else { - if (element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_TO_LEFT_OF) - && isButtonId(parent, element.getAttributeNS(ANDROID_URI, - ATTR_LAYOUT_TO_RIGHT_OF))) { - return true; - } - if (isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_LEFT) && - !isTrue(element, ATTR_LAYOUT_ALIGN_PARENT_RIGHT)) { - return true; - } - } - - return false; - } else { - // TODO: Consider other button layouts - GridLayouts, custom views extending - // LinearLayout etc? - return false; - } - } - - /** - * Returns true if the given attribute (in the Android namespace) is set to - * true on the given element - */ - private static boolean isTrue(Element element, String attribute) { - return VALUE_TRUE.equals(element.getAttributeNS(ANDROID_URI, attribute)); - } - - /** Is the given target id the id of a {@code <Button>} within this RelativeLayout? */ - private static boolean isButtonId(Element parent, String targetId) { - for (Element child : LintUtils.getChildren(parent)) { - String id = child.getAttributeNS(ANDROID_URI, ATTR_ID); - if (LintUtils.idReferencesMatch(id, targetId)) { - return child.getTagName().equals(BUTTON); - } - } - return false; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ChildCountDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ChildCountDetector.java deleted file mode 100644 index bc1e30d..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ChildCountDetector.java +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.GRID_VIEW; -import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; -import static com.android.SdkConstants.LIST_VIEW; -import static com.android.SdkConstants.REQUEST_FOCUS; -import static com.android.SdkConstants.SCROLL_VIEW; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.Arrays; -import java.util.Collection; - -/** - * Check which makes sure that views have the expected number of declared - * children (e.g. at most one in ScrollViews and none in AdapterViews) - */ -public class ChildCountDetector extends LayoutDetector { - - /** The main issue discovered by this detector */ - public static final Issue SCROLLVIEW_ISSUE = Issue.create( - "ScrollViewCount", //$NON-NLS-1$ - "Checks that ScrollViews have exactly one child widget", - "ScrollViews can only have one child widget. If you want more children, wrap them " + - "in a container layout.", - Category.CORRECTNESS, - 8, - Severity.WARNING, - ChildCountDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** The main issue discovered by this detector */ - public static final Issue ADAPTERVIEW_ISSUE = Issue.create( - "AdapterViewChildren", //$NON-NLS-1$ - "Checks that AdapterViews do not define their children in XML", - "AdapterViews such as ListViews must be configured with data from Java code, " + - "such as a ListAdapter.", - Category.CORRECTNESS, - 10, - Severity.WARNING, - ChildCountDetector.class, - Scope.RESOURCE_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/reference/android/widget/AdapterView.html"); //$NON-NLS-1$ - - /** Constructs a new {@link ChildCountDetector} */ - public ChildCountDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - SCROLL_VIEW, - HORIZONTAL_SCROLL_VIEW, - LIST_VIEW, - GRID_VIEW - // TODO: Shouldn't Spinner be in this list too? (Was not there in layoutopt) - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - int childCount = LintUtils.getChildCount(element); - String tagName = element.getTagName(); - if (tagName.equals(SCROLL_VIEW) || tagName.equals(HORIZONTAL_SCROLL_VIEW)) { - if (childCount > 1 && getAccurateChildCount(element) > 1) { - context.report(SCROLLVIEW_ISSUE, element, - context.getLocation(element), "A scroll view can have only one child", - null); - } - } else { - // Adapter view - if (childCount > 0 && getAccurateChildCount(element) > 0) { - context.report(ADAPTERVIEW_ISSUE, element, - context.getLocation(element), - "A list/grid should have no children declared in XML", null); - } - } - } - - /** Counts the number of children, but skips certain tags like {@code <requestFocus>} */ - private static int getAccurateChildCount(Element element) { - NodeList childNodes = element.getChildNodes(); - int childCount = 0; - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.ELEMENT_NODE && - !REQUEST_FOCUS.equals(child.getNodeName())) { - childCount++; - } - } - - return childCount; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CleanupDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CleanupDetector.java deleted file mode 100644 index 1541d23..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CleanupDetector.java +++ /dev/null @@ -1,540 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -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 org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.BasicValue; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.Interpreter; -import org.objectweb.asm.tree.analysis.Value; - -import java.util.Arrays; -import java.util.List; - -/** - * Checks for missing {@code recycle} calls on resources that encourage it, and - * for missing {@code commit} calls on FragmentTransactions, etc. - */ -public class CleanupDetector extends Detector implements ClassScanner { - /** Problems with missing recycle calls */ - public static final Issue RECYCLE_RESOURCE = Issue.create( - "Recycle", //$NON-NLS-1$ - "Looks for missing recycle() calls on resources", - - "Many resources, such as TypedArrays, VelocityTrackers, etc., " + - "should be recycled (with a `recycle()` call) after use. This lint check looks " + - "for missing `recycle()` calls.", - - Category.PERFORMANCE, - 7, - Severity.WARNING, - CleanupDetector.class, - Scope.CLASS_FILE_SCOPE); - - /** Problems with missing commit calls. */ - public static final Issue COMMIT_FRAGMENT = Issue.create( - "CommitTransaction", //$NON-NLS-1$ - "Looks for missing commit() calls on FragmentTransactions", - - "After creating a `FragmentTransaction`, you typically need to commit it as well", - - Category.CORRECTNESS, - 7, - Severity.WARNING, - CleanupDetector.class, - Scope.CLASS_FILE_SCOPE); - - // Target method names - private static final String RECYCLE = "recycle"; //$NON-NLS-1$ - private static final String OBTAIN = "obtain"; //$NON-NLS-1$ - private static final String OBTAIN_NO_HISTORY = "obtainNoHistory"; //$NON-NLS-1$ - private static final String OBTAIN_ATTRIBUTES = "obtainAttributes"; //$NON-NLS-1$ - private static final String OBTAIN_TYPED_ARRAY = "obtainTypedArray"; //$NON-NLS-1$ - private static final String OBTAIN_STYLED_ATTRIBUTES = "obtainStyledAttributes"; //$NON-NLS-1$ - private static final String BEGIN_TRANSACTION = "beginTransaction"; //$NON-NLS-1$ - private static final String COMMIT = "commit"; //$NON-NLS-1$ - private static final String COMMIT_ALLOWING_LOSS = "commitAllowingStateLoss"; //$NON-NLS-1$ - - // Target owners - private static final String VELOCITY_TRACKER_CLS = "android/view/VelocityTracker";//$NON-NLS-1$ - private static final String TYPED_ARRAY_CLS = "android/content/res/TypedArray"; //$NON-NLS-1$ - private static final String CONTEXT_CLS = "android/content/Context"; //$NON-NLS-1$ - private static final String MOTION_EVENT_CLS = "android/view/MotionEvent"; //$NON-NLS-1$ - private static final String HANDLER_CLS = "android/os/Handler"; //$NON-NLS-1$ - private static final String RESOURCES_CLS = "android/content/res/Resources"; //$NON-NLS-1$ - private static final String PARCEL_CLS = "android/os/Parcel"; //$NON-NLS-1$ - private static final String FRAGMENT_MANAGER_CLS = "android/app/FragmentManager"; //$NON-NLS-1$ - private static final String FRAGMENT_MANAGER_V4_CLS = "android/support/v4/app/FragmentManager"; //$NON-NLS-1$ - private static final String FRAGMENT_TRANSACTION_CLS = "android/app/FragmentTransaction"; //$NON-NLS-1$ - private static final String FRAGMENT_TRANSACTION_V4_CLS = "android/support/v4/app/FragmentTransaction"; //$NON-NLS-1$ - - // Target description signatures - private static final String TYPED_ARRAY_SIG = "Landroid/content/res/TypedArray;"; //$NON-NLS-1$ - - private boolean mObtainsTypedArray; - private boolean mRecyclesTypedArray; - private boolean mObtainsTracker; - private boolean mRecyclesTracker; - private boolean mObtainsMotionEvent; - private boolean mRecyclesMotionEvent; - private boolean mObtainsParcel; - private boolean mRecyclesParcel; - private boolean mObtainsTransaction; - private boolean mCommitsTransaction; - - /** Constructs a new {@link CleanupDetector} */ - public CleanupDetector() { - } - - @Override - public void afterCheckProject(@NonNull Context context) { - int phase = context.getDriver().getPhase(); - if (phase == 1) { - if (mObtainsTypedArray && !mRecyclesTypedArray - || mObtainsTracker && !mRecyclesTracker - || mObtainsParcel && !mRecyclesParcel - || mObtainsMotionEvent && !mRecyclesMotionEvent - || mObtainsTransaction && !mCommitsTransaction) { - context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE); - } - } - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Arrays.asList( - RECYCLE, - OBTAIN_STYLED_ATTRIBUTES, - OBTAIN, - OBTAIN_ATTRIBUTES, - OBTAIN_TYPED_ARRAY, - OBTAIN_NO_HISTORY, - BEGIN_TRANSACTION, - COMMIT, - COMMIT_ALLOWING_LOSS - ); - } - - @Override - public void checkCall( - @NonNull ClassContext context, - @NonNull ClassNode classNode, - @NonNull MethodNode method, - @NonNull MethodInsnNode call) { - String name = call.name; - String owner = call.owner; - String desc = call.desc; - int phase = context.getDriver().getPhase(); - if (RECYCLE.equals(name) && desc.equals("()V")) { //$NON-NLS-1$ - if (owner.equals(TYPED_ARRAY_CLS)) { - mRecyclesTypedArray = true; - } else if (owner.equals(VELOCITY_TRACKER_CLS)) { - mRecyclesTracker = true; - } else if (owner.equals(MOTION_EVENT_CLS)) { - mRecyclesMotionEvent = true; - } else if (owner.equals(PARCEL_CLS)) { - mRecyclesParcel = true; - } - } else if ((COMMIT.equals(name) || COMMIT_ALLOWING_LOSS.equals(name)) - && desc.equals("()I")) { //$NON-NLS-1$ - if (owner.equals(FRAGMENT_TRANSACTION_CLS) - || owner.equals(FRAGMENT_TRANSACTION_V4_CLS)) { - mCommitsTransaction = true; - } - } else if (owner.equals(MOTION_EVENT_CLS)) { - if (OBTAIN.equals(name) || OBTAIN_NO_HISTORY.equals(name)) { - mObtainsMotionEvent = true; - if (phase == 2 && !mRecyclesMotionEvent) { - context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call), - getErrorMessage(MOTION_EVENT_CLS), - null); - } else if (phase == 1 - && checkMethodFlow(context, classNode, method, call, MOTION_EVENT_CLS)) { - // Already reported error above; don't do global check - mRecyclesMotionEvent = true; - } - } - } else if (OBTAIN.equals(name)) { - if (owner.equals(VELOCITY_TRACKER_CLS)) { - mObtainsTracker = true; - if (phase == 2 && !mRecyclesTracker) { - context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call), - getErrorMessage(VELOCITY_TRACKER_CLS), - null); - } - } else if (owner.equals(PARCEL_CLS)) { - mObtainsParcel = true; - if (phase == 2 && !mRecyclesParcel) { - context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call), - getErrorMessage(PARCEL_CLS), - null); - } else if (phase == 1 - && checkMethodFlow(context, classNode, method, call, PARCEL_CLS)) { - // Already reported error above; don't do global check - mRecyclesParcel = true; - } - } - } else if (OBTAIN_STYLED_ATTRIBUTES.equals(name) - || OBTAIN_ATTRIBUTES.equals(name) - || OBTAIN_TYPED_ARRAY.equals(name)) { - if ((owner.equals(CONTEXT_CLS) || owner.equals(RESOURCES_CLS)) - && desc.endsWith(TYPED_ARRAY_SIG)) { - mObtainsTypedArray = true; - if (phase == 2 && !mRecyclesTypedArray) { - context.report(RECYCLE_RESOURCE, method, call, context.getLocation(call), - getErrorMessage(TYPED_ARRAY_CLS), - null); - } else if (phase == 1 - && checkMethodFlow(context, classNode, method, call, TYPED_ARRAY_CLS)) { - // Already reported error above; don't do global check - mRecyclesTypedArray = true; - } - } - } else if (BEGIN_TRANSACTION.equals(name) - && (owner.equals(FRAGMENT_MANAGER_CLS) || owner.equals(FRAGMENT_MANAGER_V4_CLS))) { - mObtainsTransaction = true; - if (phase == 2 && !mCommitsTransaction) { - context.report(COMMIT_FRAGMENT, method, call, context.getLocation(call), - getErrorMessage(FRAGMENT_MANAGER_CLS), null); - } else if (phase == 1 - && checkMethodFlow(context, classNode, method, call, - owner.equals(FRAGMENT_MANAGER_CLS) - ? FRAGMENT_TRANSACTION_CLS : FRAGMENT_TRANSACTION_V4_CLS)) { - // Already reported error above; don't do global check - mCommitsTransaction = true; - } - } - } - - /** Computes an error message for a missing recycle of the given type */ - private static String getErrorMessage(String owner) { - if (FRAGMENT_TRANSACTION_CLS.equals(owner) || FRAGMENT_TRANSACTION_V4_CLS.equals(owner)) { - return "This transaction should be completed with a commit() call"; - } - String className = owner.substring(owner.lastIndexOf('/') + 1); - return String.format("This %1$s should be recycled after use with #recycle()", - className); - } - - /** - * Ensures that the given allocate call in the given method has a - * corresponding recycle method, also within the same method, OR, the - * allocated resource flows out of the method (either as a return value, or - * into a field, or into some other method (with some known exceptions; e.g. - * passing a MotionEvent into another MotionEvent's constructor is fine) - * <p> - * Returns true if an error was found - */ - private static boolean checkMethodFlow(ClassContext context, ClassNode classNode, - MethodNode method, MethodInsnNode call, String recycleOwner) { - CleanupTracker interpreter = new CleanupTracker(context, method, call, recycleOwner); - ResourceAnalyzer analyzer = new ResourceAnalyzer(interpreter); - interpreter.setAnalyzer(analyzer); - try { - analyzer.analyze(classNode.name, method); - if (!interpreter.isCleanedUp() && !interpreter.isEscaped()) { - Location location = context.getLocation(call); - String message = getErrorMessage(recycleOwner); - Issue issue = call.owner.equals(FRAGMENT_MANAGER_CLS) - ? COMMIT_FRAGMENT : RECYCLE_RESOURCE; - context.report(issue, method, call, location, message, null); - return true; - } - } catch (AnalyzerException e) { - context.log(e, null); - } - - return false; - } - - @VisibleForTesting - static boolean hasReturnType(String owner, String desc) { - int descLen = desc.length(); - int ownerLen = owner.length(); - if (descLen < ownerLen + 3) { - return false; - } - if (desc.charAt(descLen - 1) != ';') { - return false; - } - int typeBegin = descLen - 2 - ownerLen; - if (desc.charAt(typeBegin - 1) != ')' || desc.charAt(typeBegin) != 'L') { - return false; - } - return desc.regionMatches(typeBegin + 1, owner, 0, ownerLen); - } - - /** - * ASM interpreter which tracks the instances of the allocated resource, and - * checks whether it is eventually passed to a {@code recycle()} call. If the - * value flows out of the method (to a field, or a method call), it will - * also consider the resource recycled. - */ - private static class CleanupTracker extends Interpreter { - // Only identity matters, not value - private static final Value INSTANCE = BasicValue.INT_VALUE; - private static final Value RECYCLED = BasicValue.FLOAT_VALUE; - private static final Value UNKNOWN = BasicValue.UNINITIALIZED_VALUE; - - private final ClassContext mContext; - private final MethodNode mMethod; - private final MethodInsnNode mObtainNode; - private boolean mIsCleanedUp; - private boolean mEscapes; - private final String mRecycleOwner; - private ResourceAnalyzer mAnalyzer; - - public CleanupTracker( - @NonNull ClassContext context, - @NonNull MethodNode method, - @NonNull MethodInsnNode obtainNode, - @NonNull String recycleOwner) { - super(Opcodes.ASM4); - mContext = context; - mMethod = method; - mObtainNode = obtainNode; - mRecycleOwner = recycleOwner; - } - - /** - * Sets the analyzer associated with the interpreter, such that it can - * get access to the execution frames - */ - void setAnalyzer(ResourceAnalyzer analyzer) { - mAnalyzer = analyzer; - } - - /** - * Returns whether a recycle call was found for the given method - * - * @return true if the resource was recycled - */ - public boolean isCleanedUp() { - return mIsCleanedUp; - } - - /** - * Returns whether the target resource escapes from the method, for - * example as a return value, or a field assignment, or getting passed - * to another method - * - * @return true if the resource escapes - */ - public boolean isEscaped() { - return mEscapes; - } - - @Override - public Value newOperation(AbstractInsnNode node) throws AnalyzerException { - return UNKNOWN; - } - - @Override - public Value newValue(final Type type) { - if (type != null && type.getSort() == Type.VOID) { - return null; - } else { - return UNKNOWN; - } - } - - @Override - public Value copyOperation(AbstractInsnNode node, Value value) throws AnalyzerException { - return value; - } - - @Override - public Value binaryOperation(AbstractInsnNode node, Value value1, Value value2) - throws AnalyzerException { - if (node.getOpcode() == Opcodes.PUTFIELD) { - if (value2 == INSTANCE) { - mEscapes = true; - } - } - return merge(value1, value2); - } - - @Override - public Value naryOperation(AbstractInsnNode node, List values) throws AnalyzerException { - if (node == mObtainNode) { - return INSTANCE; - } - - MethodInsnNode call = null; - if (node.getType() == AbstractInsnNode.METHOD_INSN) { - call = (MethodInsnNode) node; - if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { - if (call.name.equals(RECYCLE) && call.owner.equals(mRecycleOwner)) { - if (values != null && values.size() == 1 && values.get(0) == INSTANCE) { - mIsCleanedUp = true; - Frame frame = mAnalyzer.getCurrentFrame(); - if (frame != null) { - int localSize = frame.getLocals(); - for (int i = 0; i < localSize; i++) { - Value local = frame.getLocal(i); - if (local == INSTANCE) { - frame.setLocal(i, RECYCLED); - } - } - int stackSize = frame.getStackSize(); - if (stackSize == 1 && frame.getStack(0) == INSTANCE) { - frame.pop(); - frame.push(RECYCLED); - } - } - return RECYCLED; - } - } else if ((call.name.equals(COMMIT) || call.name.equals(COMMIT_ALLOWING_LOSS)) - && call.owner.equals(mRecycleOwner)) { - if (values != null && values.size() == 1 && values.get(0) == INSTANCE) { - mIsCleanedUp = true; - return INSTANCE; - } - } else if (call.owner.equals(mRecycleOwner) - && hasReturnType(mRecycleOwner, call.desc)) { - // Called method which returns self. This helps handle cases where you call - // createTransaction().method1().method2().method3().commit() -- if - // method1, 2 and 3 all return "this" then the commit call is really - // called on the createTransaction instance - return INSTANCE; - } - } - } - - if (values != null && values.size() >= 1) { - // Skip the first element: method calls *on* the TypedArray are okay - int start = node.getOpcode() == Opcodes.INVOKESTATIC ? 0 : 1; - for (int i = 0, n = values.size(); i < n; i++) { - Object v = values.get(i); - if (v == INSTANCE && i >= start) { - // Known special cases - if (node.getOpcode() == Opcodes.INVOKESTATIC) { - assert call != null; - if (call.name.equals(OBTAIN) && - call.owner.equals(MOTION_EVENT_CLS)) { - return UNKNOWN; - } - } - - // Passing the instance to another method: could leak - // the instance out of this method (for example calling - // a method which recycles it on our behalf, or store it - // in some holder which will recycle it later). In this - // case, just assume that things are okay. - mEscapes = true; - } else if (v == RECYCLED && call != null) { - Location location = mContext.getLocation(call); - String message = String.format("This %1$s has already been recycled", - mRecycleOwner.substring(mRecycleOwner.lastIndexOf('/') + 1)); - mContext.report(RECYCLE_RESOURCE, mMethod, call, location, message, null); - } - } - } - - return UNKNOWN; - } - - @Override - public Value unaryOperation(AbstractInsnNode node, Value value) throws AnalyzerException { - return value; - } - - @Override - public Value ternaryOperation(AbstractInsnNode node, Value value1, Value value2, - Value value3) throws AnalyzerException { - if (value1 == RECYCLED || value2 == RECYCLED || value3 == RECYCLED) { - return RECYCLED; - } else if (value1 == INSTANCE || value2 == INSTANCE || value3 == INSTANCE) { - return INSTANCE; - } - return UNKNOWN; - } - - @Override - public void returnOperation(AbstractInsnNode node, Value value1, Value value2) - throws AnalyzerException { - if (value1 == INSTANCE || value2 == INSTANCE) { - mEscapes = true; - } - } - - @Override - public Value merge(Value value1, Value value2) { - if (value1 == RECYCLED || value2 == RECYCLED) { - return RECYCLED; - } else if (value1 == INSTANCE || value2 == INSTANCE) { - return INSTANCE; - } - return UNKNOWN; - } - } - - private static class ResourceAnalyzer extends Analyzer { - private Frame mCurrent; - private Frame mFrame1; - private Frame mFrame2; - - public ResourceAnalyzer(Interpreter interpreter) { - super(interpreter); - } - - Frame getCurrentFrame() { - return mCurrent; - } - - @Override - protected void init(String owner, MethodNode m) throws AnalyzerException { - mCurrent = mFrame2; - super.init(owner, m); - } - - @Override - protected Frame newFrame(int nLocals, int nStack) { - // Stash the two most recent frame allocations. When init is called the second - // most recently seen frame is the current frame used during execution, which - // is where we need to replace INSTANCE with RECYCLED when the void - // recycle method is called. - Frame newFrame = super.newFrame(nLocals, nStack); - mFrame2 = mFrame1; - mFrame1 = newFrame; - return newFrame; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ColorUsageDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ColorUsageDetector.java deleted file mode 100644 index e7ffcb6..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ColorUsageDetector.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright (C) 2012 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.RESOURCE_CLZ_COLOR; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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 java.io.File; - -import lombok.ast.AstVisitor; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.Select; - -/** - * Looks for cases where the code attempts to set a resource id, rather than - * a resolved color, as the RGB int. - */ -public class ColorUsageDetector extends Detector implements Detector.JavaScanner { - /** Attempting to set a resource id as a color */ - public static final Issue ISSUE = Issue.create( - "ResourceAsColor", //$NON-NLS-1$ - "Looks for calls to setColor where a resource id is passed instead of a " + - "resolved color", - - "Methods that take a color in the form of an integer should be passed " + - "an RGB triple, not the actual color resource id. You must call " + - "`getResources().getColor(resource)` to resolve the actual color value first.", - - Category.CORRECTNESS, - 7, - Severity.ERROR, - ColorUsageDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Constructs a new {@link ColorUsageDetector} check */ - public ColorUsageDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements JavaScanner ---- - - @Override - public boolean appliesToResourceRefs() { - return true; - } - - @Override - public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull Node select, @NonNull String type, @NonNull String name, boolean isFramework) { - if (type.equals(RESOURCE_CLZ_COLOR)) { - while (select.getParent() instanceof Select) { - select = select.getParent(); - } - - // See if this method is being called on a setter - if (select.getParent() instanceof MethodInvocation) { - MethodInvocation call = (MethodInvocation) select.getParent(); - String methodName = call.astName().astValue(); - if (methodName.endsWith("Color") //$NON-NLS-1$ - && methodName.startsWith("set")) { //$NON-NLS-1$ - context.report( - ISSUE, select, context.getLocation(select), String.format( - "Should pass resolved color instead of resource id here: " + - "getResources().getColor(%1$s)", select.toString()), - null); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CommentDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CommentDetector.java deleted file mode 100644 index 6f07e77..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CommentDetector.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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 java.io.File; -import java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.Comment; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.Node; - -/** - * Looks for issues in Java comments - */ -public class CommentDetector extends Detector implements Detector.JavaScanner { - private static final String STOPSHIP_COMMENT = "STOPSHIP"; //$NON-NLS-1$ - - /** Looks for hidden code */ - public static final Issue EASTEREGG = Issue.create( - "EasterEgg", //$NON-NLS-1$ - "Looks for hidden easter eggs", - "An \"easter egg\" is code deliberately hidden in the code, both from potential " + - "users and even from other developers. This lint check looks for code which " + - "looks like it may be hidden from sight.", - Category.SECURITY, - 6, - Severity.WARNING, - CommentDetector.class, - Scope.JAVA_FILE_SCOPE).setEnabledByDefault(false); - - /** Looks for special comment markers intended to stop shipping the code */ - public static final Issue STOPSHIP = Issue.create( - "StopShip", //$NON-NLS-1$ - "Looks for comment markers of the form \"STOPSHIP\" which indicates that code " + - "should not be released yet", - - "Using the comment `// STOPSHIP` can be used to flag code that is incomplete but " + - "checked in. This comment marker can be used to indicate that the code should not " + - "be shipped until the issue is addressed, and lint will look for these.", - Category.CORRECTNESS, - 10, - Severity.WARNING, - CommentDetector.class, - Scope.JAVA_FILE_SCOPE).setEnabledByDefault(false); - - private static final String ESCAPE_STRING = "\\u002a\\u002f"; //$NON-NLS-1$ - - /** Lombok's AST only passes comment nodes for Javadoc so I need to do manual token scanning - instead */ - private static final boolean USE_AST = false; - - - /** Constructs a new {@link CommentDetector} check */ - public CommentDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - @Override - public List<Class<? extends Node>> getApplicableNodeTypes() { - if (USE_AST) { - return Collections.<Class<? extends Node>>singletonList(Comment.class); - } else { - return null; - } - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - // Lombok does not generate comment nodes for block and line comments, only for - // javadoc comments! - if (USE_AST) { - return new CommentChecker(context); - } else { - String source = context.getContents(); - if (source == null) { - return null; - } - // Process the Java source such that we pass tokens to it - - for (int i = 0, n = source.length() - 1; i < n; i++) { - char c = source.charAt(i); - if (c == '\\') { - i += 1; - } else if (c == '/') { - char next = source.charAt(i + 1); - if (next == '/') { - // Line comment - int start = i + 2; - int end = source.indexOf('\n', start); - if (end == -1) { - end = n; - } - checkComment(context, source, 0, start, end); - } else if (next == '*') { - // Block comment - int start = i + 2; - int end = source.indexOf("*/", start); - if (end == -1) { - end = n; - } - checkComment(context, source, 0, start, end); - } - } - } - return null; - } - } - - private static class CommentChecker extends ForwardingAstVisitor { - private final JavaContext mContext; - - public CommentChecker(JavaContext context) { - mContext = context; - } - - @Override - public boolean visitComment(Comment node) { - String contents = node.astContent(); - checkComment(mContext, contents, node.getPosition().getStart(), 0, contents.length()); - return super.visitComment(node); - } - } - - private static void checkComment( - @NonNull Context context, - @NonNull String source, - int offset, - int start, - int end) { - char prev = 0; - char c; - for (int i = start; i < end - 2; i++, prev = c) { - c = source.charAt(i); - if (prev == '\\') { - if (c == 'u' || c == 'U') { - if (source.regionMatches(true, i - 1, ESCAPE_STRING, - 0, ESCAPE_STRING.length())) { - Location location = Location.create(context.file, source, - offset + i - 1, offset + i - 1 + ESCAPE_STRING.length()); - context.report(EASTEREGG, location, - "Code might be hidden here; found unicode escape sequence " + - "which is interpreted as comment end, compiled code follows", - null); - } - } else { - i++; - } - } else if (prev == 'S' && c == 'T' && - source.regionMatches(i - 1, STOPSHIP_COMMENT, 0, STOPSHIP_COMMENT.length())) { - // TODO: Only flag this issue in release mode?? - Location location = Location.create(context.file, source, - offset + i - 1, offset + i - 1 + STOPSHIP_COMMENT.length()); - context.report(STOPSHIP, location, - "STOPSHIP comment found; points to code which must be fixed prior " + - "to release", - null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ControlFlowGraph.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ControlFlowGraph.java deleted file mode 100644 index cbafe28..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ControlFlowGraph.java +++ /dev/null @@ -1,329 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.google.common.collect.Maps; - -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FrameNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LabelNode; -import org.objectweb.asm.tree.LineNumberNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TryCatchBlockNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.BasicInterpreter; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * A {@linkplain ControlFlowGraph} is a graph containing a node for each - * instruction in a method, and an edge for each possible control flow; usually - * just "next" for the instruction following the current instruction, but in the - * case of a branch such as an "if", multiple edges to each successive location, - * or with a "goto", a single edge to the jumped-to instruction. - * <p> - * It also adds edges for abnormal control flow, such as the possibility of a - * method call throwing a runtime exception. - */ -public class ControlFlowGraph { - /** Map from instructions to nodes */ - private Map<AbstractInsnNode, Node> mNodeMap; - - /** - * Creates a new {@link ControlFlowGraph} and populates it with the flow - * control for the given method. If the optional {@code initial} parameter is - * provided with an existing graph, then the graph is simply populated, not - * created. This allows subclassing of the graph instance, if necessary. - * - * @param initial usually null, but can point to an existing instance of a - * {@link ControlFlowGraph} in which that graph is reused (but - * populated with new edges) - * @param classNode the class containing the method to be analyzed - * @param method the method to be analyzed - * @return a {@link ControlFlowGraph} with nodes for the control flow in the - * given method - * @throws AnalyzerException if the underlying bytecode library is unable to - * analyze the method bytecode - */ - @NonNull - public static ControlFlowGraph create( - @Nullable ControlFlowGraph initial, - @NonNull ClassNode classNode, - @NonNull MethodNode method) throws AnalyzerException { - final ControlFlowGraph graph = initial != null ? initial : new ControlFlowGraph(); - final InsnList instructions = method.instructions; - graph.mNodeMap = Maps.newHashMapWithExpectedSize(instructions.size()); - - // Create a flow control graph using ASM4's analyzer. According to the ASM 4 guide - // (download.forge.objectweb.org/asm/asm4-guide.pdf) there are faster ways to construct - // it, but those require a lot more code. - Analyzer analyzer = new Analyzer(new BasicInterpreter()) { - @Override - protected void newControlFlowEdge(int insn, int successor) { - // Update the information as of whether the this object has been - // initialized at the given instruction. - AbstractInsnNode from = instructions.get(insn); - AbstractInsnNode to = instructions.get(successor); - graph.add(from, to); - } - - @Override - protected boolean newControlFlowExceptionEdge(int insn, TryCatchBlockNode tcb) { - AbstractInsnNode from = instructions.get(insn); - graph.exception(from, tcb); - return super.newControlFlowExceptionEdge(insn, tcb); - } - - @Override - protected boolean newControlFlowExceptionEdge(int insn, int successor) { - AbstractInsnNode from = instructions.get(insn); - AbstractInsnNode to = instructions.get(successor); - graph.exception(from, to); - return super.newControlFlowExceptionEdge(insn, successor); - } - }; - - analyzer.analyze(classNode.name, method); - return graph; - } - - /** A {@link Node} is a node in the control flow graph for a method, pointing to - * the instruction and its possible successors */ - public static class Node { - /** The instruction */ - public final AbstractInsnNode instruction; - /** Any normal successors (e.g. following instruction, or goto or conditional flow) */ - public final List<Node> successors = new ArrayList<Node>(2); - /** Any abnormal successors (e.g. the handler to go to following an exception) */ - public final List<Node> exceptions = new ArrayList<Node>(1); - - /** A tag for use during depth-first-search iteration of the graph etc */ - public int visit; - - /** - * Constructs a new control graph node - * - * @param instruction the instruction to associate with this node - */ - public Node(@NonNull AbstractInsnNode instruction) { - this.instruction = instruction; - } - - void addSuccessor(@NonNull Node node) { - if (!successors.contains(node)) { - successors.add(node); - } - } - - void addExceptionPath(@NonNull Node node) { - if (!exceptions.contains(node)) { - exceptions.add(node); - } - } - - /** - * Represents this instruction as a string, for debugging purposes - * - * @param includeAdjacent whether it should include a display of - * adjacent nodes as well - * @return a string representation - */ - @NonNull - public String toString(boolean includeAdjacent) { - StringBuilder sb = new StringBuilder(100); - - sb.append(getId(instruction)); - sb.append(':'); - - if (instruction instanceof LabelNode) { - //LabelNode l = (LabelNode) instruction; - //sb.append('L' + l.getLabel().getOffset() + ":"); - //sb.append('L' + l.getLabel().info + ":"); - sb.append("LABEL"); - } else if (instruction instanceof LineNumberNode) { - sb.append("LINENUMBER ").append(((LineNumberNode)instruction).line); - } else if (instruction instanceof FrameNode) { - sb.append("FRAME"); - } else { - int opcode = instruction.getOpcode(); - // AbstractVisitor isn't available unless debug/util is included, - boolean printed = false; - try { - Class<?> cls = Class.forName("org.objectweb.asm.util"); //$NON-NLS-1$ - Field field = cls.getField("OPCODES"); - String[] OPCODES = (String[]) field.get(null); - printed = true; - if (opcode > 0 && opcode <= OPCODES.length) { - sb.append(OPCODES[opcode]); - if (instruction.getType() == AbstractInsnNode.METHOD_INSN) { - sb.append('(').append(((MethodInsnNode)instruction).name).append(')'); - } - } - } catch (Throwable t) { - // debug not installed: just do toString() on the instructions - } - if (!printed) { - if (instruction.getType() == AbstractInsnNode.METHOD_INSN) { - sb.append('(').append(((MethodInsnNode)instruction).name).append(')'); - } else { - sb.append(instruction.toString()); - } - } - } - - if (includeAdjacent) { - if (successors != null && !successors.isEmpty()) { - sb.append(" Next:"); - for (Node successor : successors) { - sb.append(' '); - sb.append(successor.toString(false)); - } - } - - if (exceptions != null && !exceptions.isEmpty()) { - sb.append(" Exceptions:"); - for (Node exception : exceptions) { - sb.append(' '); - sb.append(exception.toString(false)); - } - } - sb.append('\n'); - } - - return sb.toString(); - } - } - - /** Adds an exception flow to this graph */ - protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) { - getNode(from).addSuccessor(getNode(to)); - } - - /** Adds an exception flow to this graph */ - protected void exception(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) { - // For now, these edges appear useless; we also get more specific - // information via the TryCatchBlockNode which we use instead. - //getNode(from).addExceptionPath(getNode(to)); - } - - /** Adds an exception try block node to this graph */ - protected void exception(@NonNull AbstractInsnNode from, @NonNull TryCatchBlockNode tcb) { - // Add tcb's to all instructions in the range - LabelNode start = tcb.start; - LabelNode end = tcb.end; // exclusive - - // Add exception edges for all method calls in the range - AbstractInsnNode curr = start; - Node handlerNode = getNode(tcb.handler); - while (curr != end && curr != null) { - if (curr.getType() == AbstractInsnNode.METHOD_INSN) { - // Method call; add exception edge to handler - if (tcb.type == null) { - // finally block: not an exception path - getNode(curr).addSuccessor(handlerNode); - } - getNode(curr).addExceptionPath(handlerNode); - } - curr = curr.getNext(); - } - } - - /** - * Looks up (and if necessary) creates a graph node for the given instruction - * - * @param instruction the instruction - * @return the control flow graph node corresponding to the given - * instruction - */ - @NonNull - public Node getNode(@NonNull AbstractInsnNode instruction) { - Node node = mNodeMap.get(instruction); - if (node == null) { - node = new Node(instruction); - mNodeMap.put(instruction, node); - } - - return node; - } - - /** - * Creates a human readable version of the graph - * - * @param start the starting instruction, or null if not known or to use the - * first instruction - * @return a string version of the graph - */ - @NonNull - public String toString(@Nullable Node start) { - StringBuilder sb = new StringBuilder(400); - - AbstractInsnNode curr; - if (start != null) { - curr = start.instruction; - } else { - if (mNodeMap.isEmpty()) { - return "<empty>"; - } else { - curr = mNodeMap.keySet().iterator().next(); - while (curr.getPrevious() != null) { - curr = curr.getPrevious(); - } - } - } - - while (curr != null) { - Node node = mNodeMap.get(curr); - if (node != null) { - sb.append(node.toString(true)); - } - curr = curr.getNext(); - } - - return sb.toString(); - } - - @Override - public String toString() { - return toString(null); - } - - // ---- For debugging only ---- - - private static Map<Object, String> sIds = null; - private static int sNextId = 1; - private static String getId(Object object) { - if (sIds == null) { - sIds = Maps.newHashMap(); - } - String id = sIds.get(object); - if (id == null) { - id = Integer.toString(sNextId++); - sIds.put(object, id); - } - return id; - } -} - diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CutPasteDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CutPasteDetector.java deleted file mode 100644 index 84fb6b6..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/CutPasteDetector.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * Copyright (C) 2012 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.RESOURCE_CLZ_ID; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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.google.common.collect.Maps; - -import java.io.File; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import lombok.ast.ArrayAccess; -import lombok.ast.AstVisitor; -import lombok.ast.BinaryExpression; -import lombok.ast.Cast; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.If; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.Select; -import lombok.ast.Statement; -import lombok.ast.VariableDefinitionEntry; -import lombok.ast.VariableReference; - -/** - * Detector looking for cut & paste issues - */ -public class CutPasteDetector extends Detector implements Detector.JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "CutPasteId", //$NON-NLS-1$ - "Looks for code cut & paste mistakes in findViewById() calls", - - "This lint check looks for cases where you have cut & pasted calls to " + - "`findViewById` but have forgotten to update the R.id field. It's possible " + - "that your code is simply (redundantly) looking up the field repeatedly, " + - "but lint cannot distinguish that from a case where you for example want to " + - "initialize fields `prev` and `next` and you cut & pasted `findViewById(R.id.prev)` " + - "and forgot to update the second initialization to `R.id.next`.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - CutPasteDetector.class, - Scope.JAVA_FILE_SCOPE); - - private Node mLastMethod; - private Map<String, MethodInvocation> mIds; - private Map<String, String> mLhs; - private Map<String, String> mCallOperands; - - /** Constructs a new {@link CutPasteDetector} check */ - public CutPasteDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - // ---- Implements JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("findViewById"); //$NON-NLS-1$ - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation call) { - String lhs = getLhs(call); - if (lhs == null) { - return; - } - - Node method = JavaContext.findSurroundingMethod(call); - if (method == null) { - return; - } else if (method != mLastMethod) { - mIds = Maps.newHashMap(); - mLhs = Maps.newHashMap(); - mCallOperands = Maps.newHashMap(); - mLastMethod = method; - } - - String callOperand = call.astOperand() != null ? call.astOperand().toString() : ""; - - Expression first = call.astArguments().first(); - if (first instanceof Select) { - Select select = (Select) first; - String id = select.astIdentifier().astValue(); - Expression operand = select.astOperand(); - if (operand instanceof Select) { - Select type = (Select) operand; - if (type.astIdentifier().astValue().equals(RESOURCE_CLZ_ID)) { - if (mIds.containsKey(id)) { - if (lhs.equals(mLhs.get(id))) { - return; - } - if (!callOperand.equals(mCallOperands.get(id))) { - return; - } - MethodInvocation earlierCall = mIds.get(id); - if (!isReachableFrom(method, earlierCall, call)) { - return; - } - Location location = context.getLocation(call); - Location secondary = context.getLocation(earlierCall); - secondary.setMessage("First usage here"); - location.setSecondary(secondary); - context.report(ISSUE, call, location, String.format( - "The id %1$s has already been looked up in this method; possible " + - "cut & paste error?", first.toString()), null); - } else { - mIds.put(id, call); - mLhs.put(id, lhs); - mCallOperands.put(id, callOperand); - } - } - } - } - } - - @Nullable - private static String getLhs(@NonNull MethodInvocation call) { - Node parent = call.getParent(); - if (parent instanceof Cast) { - parent = parent.getParent(); - } - - if (parent instanceof VariableDefinitionEntry) { - VariableDefinitionEntry vde = (VariableDefinitionEntry) parent; - return vde.astName().astValue(); - } else if (parent instanceof BinaryExpression) { - BinaryExpression be = (BinaryExpression) parent; - Expression left = be.astLeft(); - if (left instanceof VariableReference || left instanceof Select) { - return be.astLeft().toString(); - } else if (left instanceof ArrayAccess) { - ArrayAccess aa = (ArrayAccess) left; - return aa.astOperand().toString(); - } - } - - return null; - } - - private static boolean isReachableFrom( - @NonNull Node method, - @NonNull MethodInvocation from, - @NonNull MethodInvocation to) { - ReachableVisitor visitor = new ReachableVisitor(from, to); - method.accept(visitor); - - return visitor.isReachable(); - } - - private static class ReachableVisitor extends ForwardingAstVisitor { - @NonNull private final MethodInvocation mFrom; - @NonNull private final MethodInvocation mTo; - private boolean mReachable; - private boolean mSeenEnd; - - public ReachableVisitor(@NonNull MethodInvocation from, @NonNull MethodInvocation to) { - mFrom = from; - mTo = to; - } - - boolean isReachable() { - return mReachable; - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (node == mFrom) { - mReachable = true; - } else if (node == mTo) { - mSeenEnd = true; - - } - return super.visitMethodInvocation(node); - } - - @Override - public boolean visitIf(If node) { - Expression condition = node.astCondition(); - Statement body = node.astStatement(); - Statement elseBody = node.astElseStatement(); - if (condition != null) { - condition.accept(this); - } - if (body != null) { - boolean wasReachable = mReachable; - body.accept(this); - mReachable = wasReachable; - } - if (elseBody != null) { - boolean wasReachable = mReachable; - elseBody.accept(this); - mReachable = wasReachable; - } - - endVisit(node); - - return false; - } - - @Override - public boolean visitNode(Node node) { - return mSeenEnd; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DeprecationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DeprecationDetector.java deleted file mode 100644 index 6475a7a..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DeprecationDetector.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ABSOLUTE_LAYOUT; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_AUTO_TEXT; -import static com.android.SdkConstants.ATTR_CAPITALIZE; -import static com.android.SdkConstants.ATTR_EDITABLE; -import static com.android.SdkConstants.ATTR_ENABLED; -import static com.android.SdkConstants.ATTR_INPUT_METHOD; -import static com.android.SdkConstants.ATTR_NUMERIC; -import static com.android.SdkConstants.ATTR_PASSWORD; -import static com.android.SdkConstants.ATTR_PHONE_NUMBER; -import static com.android.SdkConstants.ATTR_SINGLE_LINE; -import static com.android.SdkConstants.EDIT_TEXT; -import static com.android.SdkConstants.VALUE_TRUE; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; - -/** - * Check which looks for usage of deprecated tags, attributes, etc. - */ -public class DeprecationDetector extends LayoutDetector { - /** Usage of deprecated views or attributes */ - public static final Issue ISSUE = Issue.create( - "Deprecated", //$NON-NLS-1$ - "Looks for usages of deprecated layouts, attributes, and so on.", - "Deprecated views, attributes and so on are deprecated because there " + - "is a better way to do something. Do it that new way. You've been warned.", - Category.CORRECTNESS, - 2, - Severity.WARNING, - DeprecationDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link DeprecationDetector} */ - public DeprecationDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList( - ABSOLUTE_LAYOUT - ); - } - - @Override - public Collection<String> getApplicableAttributes() { - return Arrays.asList( - // TODO: fill_parent is deprecated as of API 8. - // We could warn about it, but it will probably be very noisy - // and make people disable the deprecation check; let's focus on - // some older flags for now - //"fill_parent", - - ATTR_EDITABLE, - ATTR_INPUT_METHOD, - ATTR_AUTO_TEXT, - ATTR_CAPITALIZE, - - // This flag is still used a lot and is still properly handled by TextView - // so in the interest of not being too noisy and make people ignore all the - // output, keep quiet about this one -for now-. - //ATTR_SINGLE_LINE, - - // This attribute is marked deprecated in android.R.attr but apparently - // using the suggested replacement of state_enabled doesn't work, see issue 27613 - //ATTR_ENABLED, - - ATTR_NUMERIC, - ATTR_PHONE_NUMBER, - ATTR_PASSWORD - - // These attributes are also deprecated; not yet enabled until we - // know the API level to apply the deprecation for: - - // "ignored as of ICS (but deprecated earlier)" - //"fadingEdge", - - // "This attribute is not used by the Android operating system." - //"restoreNeedsApplication", - - // "This will create a non-standard UI appearance, because the search bar UI is - // changing to use only icons for its buttons." - //"searchButtonText", - - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - context.report(ISSUE, element, context.getLocation(element), - String.format("%1$s is deprecated", element.getTagName()), null); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (!ANDROID_URI.equals(attribute.getNamespaceURI())) { - return; - } - - String name = attribute.getLocalName(); - String fix; - int minSdk = 1; - if (name.equals(ATTR_EDITABLE)) { - if (!EDIT_TEXT.equals(attribute.getOwnerElement().getTagName())) { - fix = "Use an <EditText> to make it editable"; - } else { - if (VALUE_TRUE.equals(attribute.getValue())) { - fix = "<EditText> is already editable"; - } else { - fix = "Use inputType instead"; - } - } - } else if (name.equals(ATTR_ENABLED)) { - fix = "Use state_enabled instead"; - } else if (name.equals(ATTR_SINGLE_LINE)) { - fix = "Use maxLines=\"1\" instead"; - } else { - assert name.equals(ATTR_INPUT_METHOD) - || name.equals(ATTR_CAPITALIZE) - || name.equals(ATTR_NUMERIC) - || name.equals(ATTR_PHONE_NUMBER) - || name.equals(ATTR_PASSWORD) - || name.equals(ATTR_AUTO_TEXT); - fix = "Use inputType instead"; - // The inputType attribute was introduced in API 3 so don't warn about - // deprecation if targeting older platforms - minSdk = 3; - } - - if (context.getProject().getMinSdk() < minSdk) { - return; - } - - context.report(ISSUE, attribute, context.getLocation(attribute), - String.format("%1$s is deprecated: %2$s", - attribute.getName(), fix), null); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DetectMissingPrefix.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DetectMissingPrefix.java deleted file mode 100644 index 2b24732..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DetectMissingPrefix.java +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_PKG_PREFIX; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_CLASS; -import static com.android.SdkConstants.ATTR_CORE_APP; -import static com.android.SdkConstants.ATTR_LAYOUT; -import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.ATTR_PACKAGE; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.TOOLS_URI; -import static com.android.SdkConstants.VIEW_TAG; -import static com.android.SdkConstants.XMLNS_PREFIX; -import static com.android.resources.ResourceFolderType.ANIM; -import static com.android.resources.ResourceFolderType.ANIMATOR; -import static com.android.resources.ResourceFolderType.COLOR; -import static com.android.resources.ResourceFolderType.DRAWABLE; -import static com.android.resources.ResourceFolderType.INTERPOLATOR; -import static com.android.resources.ResourceFolderType.LAYOUT; -import static com.android.resources.ResourceFolderType.MENU; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.Collection; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.Set; - -/** - * Detects layout attributes on builtin Android widgets that do not specify - * a prefix but probably should. - */ -public class DetectMissingPrefix extends LayoutDetector { - - /** Attributes missing the android: prefix */ - public static final Issue MISSING_NAMESPACE = Issue.create( - "MissingPrefix", //$NON-NLS-1$ - "Detect XML attributes not using the Android namespace", - "Most Android views have attributes in the Android namespace. When referencing " + - "these attributes you *must* include the namespace prefix, or your attribute will " + - "be interpreted by `aapt` as just a custom attribute.\n" + - "\n" + - "Similarly, in manifest files, nearly all attributes should be in the `android:` " + - "namespace.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - DetectMissingPrefix.class, - EnumSet.of(Scope.MANIFEST, Scope.RESOURCE_FILE)) - .addAnalysisScope(Scope.MANIFEST_SCOPE) - .addAnalysisScope(Scope.RESOURCE_FILE_SCOPE); - - private static final Set<String> NO_PREFIX_ATTRS = new HashSet<String>(); - static { - NO_PREFIX_ATTRS.add(ATTR_CLASS); - NO_PREFIX_ATTRS.add(ATTR_STYLE); - NO_PREFIX_ATTRS.add(ATTR_LAYOUT); - NO_PREFIX_ATTRS.add(ATTR_PACKAGE); - NO_PREFIX_ATTRS.add(ATTR_CORE_APP); - } - - /** Constructs a new {@link DetectMissingPrefix} */ - public DetectMissingPrefix() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == LAYOUT - || folderType == MENU - || folderType == DRAWABLE - || folderType == ANIM - || folderType == ANIMATOR - || folderType == COLOR - || folderType == INTERPOLATOR; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String uri = attribute.getNamespaceURI(); - if (uri == null || uri.isEmpty()) { - String name = attribute.getName(); - if (name == null) { - return; - } - if (NO_PREFIX_ATTRS.contains(name)) { - return; - } - - Element element = attribute.getOwnerElement(); - if (isCustomView(element) && context.getResourceFolderType() != null) { - return; - } - - if (name.startsWith(XMLNS_PREFIX)) { - return; - } - - context.report(MISSING_NAMESPACE, attribute, - context.getLocation(attribute), - "Attribute is missing the Android namespace prefix", - null); - } else if (!ANDROID_URI.equals(uri) - && !TOOLS_URI.equals(uri) - && context.getResourceFolderType() == ResourceFolderType.LAYOUT - && !isCustomView(attribute.getOwnerElement()) - && !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) - // TODO: Consider not enforcing that the parent is a custom view - // too, though in that case we should filter out views that are - // layout params for the custom view parent: - // ....&& !attribute.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) - && attribute.getOwnerElement().getParentNode().getNodeType() == Node.ELEMENT_NODE - && !isCustomView((Element) attribute.getOwnerElement().getParentNode())) { - context.report(MISSING_NAMESPACE, attribute, - context.getLocation(attribute), - String.format("Unexpected namespace prefix \"%1$s\" found for tag %2$s", - attribute.getPrefix(), attribute.getOwnerElement().getTagName()), - null); - } - } - - private static boolean isCustomView(Element element) { - // If this is a custom view, the usage of custom attributes can be legitimate - String tag = element.getTagName(); - if (tag.equals(VIEW_TAG)) { - // <view class="my.custom.view" ...> - return true; - } - - return tag.indexOf('.') != -1 && !tag.startsWith(ANDROID_PKG_PREFIX); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DosLineEndingDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DosLineEndingDetector.java deleted file mode 100644 index 1a2a720..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DosLineEndingDetector.java +++ /dev/null @@ -1,115 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Document; - -/** - * Checks that the line endings in DOS files are consistent - */ -public class DosLineEndingDetector extends LayoutDetector { - /** Detects mangled DOS line ending documents */ - public static final Issue ISSUE = Issue.create( - "MangledCRLF", //$NON-NLS-1$ - "Checks that files with DOS line endings are consistent", - - "On Windows, line endings are typically recorded as carriage return plus " + - "newline: \\r\\n.\n" + - "\n" + - "This detector looks for invalid line endings with repeated carriage return " + - "characters (without newlines). Previous versions of the ADT plugin could " + - "accidentally introduce these into the file, and when editing the file, the " + - "editor could produce confusing visual artifacts.", - - Category.CORRECTNESS, - 2, - Severity.ERROR, - DosLineEndingDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setMoreInfo("https://bugs.eclipse.org/bugs/show_bug.cgi?id=375421"); //$NON-NLS-1$ - - /** Constructs a new {@link DosLineEndingDetector} */ - public DosLineEndingDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - String contents = context.getContents(); - if (contents == null) { - return; - } - - // We could look for *consistency* and complain if you mix \n and \r\n too, - // but that isn't really a problem (most editors handle it) so let's - // not complain needlessly. - - char prev = 0; - for (int i = 0, n = contents.length(); i < n; i++) { - char c = contents.charAt(i); - if (c == '\r' && prev == '\r') { - String message = "Incorrect line ending: found carriage return (\\r) without " + - "corresponding newline (\\n)"; - - // Mark the whole line as the error range, since pointing just to the - // line ending makes the error invisible in IDEs and error reports etc - // Find the most recent non-blank line - boolean blankLine = true; - for (int index = i - 2; index < i; index++) { - char d = contents.charAt(index); - if (!Character.isWhitespace(d)) { - blankLine = false; - } - } - - int lineBegin = i; - for (int index = i - 2; index >= 0; index--) { - char d = contents.charAt(index); - if (d == '\n') { - lineBegin = index + 1; - if (!blankLine) { - break; - } - } else if (!Character.isWhitespace(d)) { - blankLine = false; - } - } - - int lineEnd = Math.min(contents.length(), i + 1); - Location location = Location.create(context.file, contents, lineBegin, lineEnd); - context.report(ISSUE, document.getDocumentElement(), location, message, null); - return; - } - prev = c; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateIdDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateIdDetector.java deleted file mode 100644 index de3e4d2..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateIdDetector.java +++ /dev/null @@ -1,673 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_LAYOUT; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.NEW_ID_PREFIX; -import static com.android.SdkConstants.VIEW_INCLUDE; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Deque; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Checks for duplicate ids within a layout and within an included layout - */ -public class DuplicateIdDetector extends LayoutDetector { - private Set<String> mIds; - private Map<File, Set<String>> mFileToIds; - private Map<File, List<String>> mIncludes; - - // Data structures used for location collection in phase 2 - - // Map from include files to include names to pairs of message and location - // Map from file defining id, to the id to be defined, to a pair of location and message - private Multimap<File, Multimap<String, Occurrence>> mLocations; - private List<Occurrence> mErrors; - - /** The main issue discovered by this detector */ - public static final Issue WITHIN_LAYOUT = Issue.create( - "DuplicateIds", //$NON-NLS-1$ - "Checks for duplicate ids within a single layout", - "Within a layout, id's should be unique since otherwise `findViewById()` can " + - "return an unexpected view.", - Category.CORRECTNESS, - 7, - Severity.WARNING, - DuplicateIdDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** The main issue discovered by this detector */ - public static final Issue CROSS_LAYOUT = Issue.create( - "DuplicateIncludedIds", //$NON-NLS-1$ - "Checks for duplicate ids across layouts that are combined with include tags", - "It's okay for two independent layouts to use the same ids. However, if " + - "layouts are combined with include tags, then the id's need to be unique " + - "within any chain of included layouts, or `Activity#findViewById()` can " + - "return an unexpected view.", - Category.CORRECTNESS, - 6, - Severity.WARNING, - DuplicateIdDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Constructs a duplicate id check */ - public DuplicateIdDetector() { - } - - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.MENU; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_ID); - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(VIEW_INCLUDE); - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - if (context.getPhase() == 1) { - mIds = new HashSet<String>(); - } - } - - @Override - public void afterCheckFile(@NonNull Context context) { - if (context.getPhase() == 1) { - // Store this layout's set of ids for full project analysis in afterCheckProject - mFileToIds.put(context.file, mIds); - - mIds = null; - } - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mFileToIds = new HashMap<File, Set<String>>(); - mIncludes = new HashMap<File, List<String>>(); - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - // Look for duplicates - if (!mIncludes.isEmpty()) { - // Traverse all the include chains and ensure that there are no duplicates - // across. - if (context.isEnabled(CROSS_LAYOUT) - && context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { - IncludeGraph graph = new IncludeGraph(context); - graph.check(); - } - } - } else { - assert context.getPhase() == 2; - - if (mErrors != null) { - for (Occurrence occurrence : mErrors) { - //assert location != null : occurrence; - Location location = occurrence.location; - if (location == null) { - location = Location.create(occurrence.file); - } else { - Object clientData = location.getClientData(); - if (clientData instanceof Node) { - Node node = (Node) clientData; - if (context.getDriver().isSuppressed(CROSS_LAYOUT, node)) { - continue; - } - } - } - - List<Occurrence> sorted = new ArrayList<Occurrence>(); - Occurrence curr = occurrence.next; - while (curr != null) { - sorted.add(curr); - curr = curr.next; - } - Collections.sort(sorted); - Location prev = location; - for (Occurrence o : sorted) { - if (o.location != null) { - prev.setSecondary(o.location); - prev = o.location; - } - } - - context.report(CROSS_LAYOUT, location, occurrence.message, null); - } - } - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - // Record include graph such that we can look for inter-layout duplicates after the - // project has been fully checked - - String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace - if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts - layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); - - if (context.getPhase() == 1) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - List<String> to = mIncludes.get(context.file); - if (to == null) { - to = new ArrayList<String>(); - mIncludes.put(context.file, to); - } - to.add(layout); - } else { - assert context.getPhase() == 2; - - Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); - if (maps != null && !maps.isEmpty()) { - for (Multimap<String, Occurrence> map : maps) { - if (!maps.isEmpty()) { - Collection<Occurrence> occurrences = map.get(layout); - if (occurrences != null && !occurrences.isEmpty()) { - for (Occurrence occurrence : occurrences) { - Location location = context.getLocation(element); - location.setClientData(element); - location.setMessage(occurrence.message); - location.setSecondary(occurrence.location); - occurrence.location = location; - } - } - } - } - } - } - } - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID); - String id = attribute.getValue(); - if (context.getPhase() == 1) { - if (mIds.contains(id)) { - Location location = context.getLocation(attribute); - - Attr first = findIdAttribute(attribute.getOwnerDocument(), id); - if (first != null && first != attribute) { - Location secondLocation = context.getLocation(first); - secondLocation.setMessage(String.format("%1$s originally defined here", id)); - location.setSecondary(secondLocation); - } - - context.report(WITHIN_LAYOUT, attribute, location, - String.format("Duplicate id %1$s, already defined earlier in this layout", - id), null); - } else if (id.startsWith(NEW_ID_PREFIX)) { - // Skip id's on include tags - if (attribute.getOwnerElement().getTagName().equals(VIEW_INCLUDE)) { - return; - } - - mIds.add(id); - } - } else { - Collection<Multimap<String, Occurrence>> maps = mLocations.get(context.file); - if (maps != null && !maps.isEmpty()) { - for (Multimap<String, Occurrence> map : maps) { - if (!maps.isEmpty()) { - Collection<Occurrence> occurrences = map.get(id); - if (occurrences != null && !occurrences.isEmpty()) { - for (Occurrence occurrence : occurrences) { - if (context.getDriver().isSuppressed(CROSS_LAYOUT, attribute)) { - return; - } - Location location = context.getLocation(attribute); - location.setClientData(attribute); - location.setMessage(occurrence.message); - location.setSecondary(occurrence.location); - occurrence.location = location; - } - } - } - } - } - } - } - - /** Find the first id attribute with the given value below the given node */ - private static Attr findIdAttribute(Node node, String targetValue) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - Attr attribute = ((Element) node).getAttributeNodeNS(ANDROID_URI, ATTR_ID); - if (attribute != null && attribute.getValue().equals(targetValue)) { - return attribute; - } - } - - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - Node child = children.item(i); - Attr result = findIdAttribute(child, targetValue); - if (result != null) { - return result; - } - } - - return null; - } - - /** Include Graph Node */ - private static class Layout { - private final File mFile; - private final Set<String> mIds; - private List<Layout> mIncludes; - private List<Layout> mIncludedBy; - - Layout(File file, Set<String> ids) { - mFile = file; - mIds = ids; - } - - Set<String> getIds() { - return mIds; - } - - String getLayoutName() { - return LintUtils.getLayoutName(mFile); - } - - String getDisplayName() { - return mFile.getParentFile().getName() + File.separator + mFile.getName(); - } - - void include(Layout target) { - if (mIncludes == null) { - mIncludes = new ArrayList<Layout>(); - } - mIncludes.add(target); - - if (target.mIncludedBy == null) { - target.mIncludedBy = new ArrayList<Layout>(); - } - target.mIncludedBy.add(this); - } - - boolean isIncluded() { - return mIncludedBy != null && !mIncludedBy.isEmpty(); - } - - File getFile() { - return mFile; - } - - List<Layout> getIncludes() { - return mIncludes; - } - - @Override - public String toString() { - return getDisplayName(); - } - } - - private class IncludeGraph { - private final Context mContext; - private final Map<File, Layout> mFileToLayout; - - public IncludeGraph(Context context) { - mContext = context; - - // Produce a DAG of the files to be included, and compute edges to all eligible - // includes. - // Then visit the DAG and whenever you find a duplicate emit a warning about the - // include path which reached it. - mFileToLayout = new HashMap<File, Layout>(2 * mIncludes.size()); - for (File file : mIncludes.keySet()) { - if (!mFileToLayout.containsKey(file)) { - mFileToLayout.put(file, new Layout(file, mFileToIds.get(file))); - } - } - for (File file : mFileToIds.keySet()) { - Set<String> ids = mFileToIds.get(file); - if (ids != null && !ids.isEmpty()) { - if (!mFileToLayout.containsKey(file)) { - mFileToLayout.put(file, new Layout(file, ids)); - } - } - } - Multimap<String, Layout> nameToLayout = - ArrayListMultimap.create(mFileToLayout.size(), 4); - for (File file : mFileToLayout.keySet()) { - String name = LintUtils.getLayoutName(file); - nameToLayout.put(name, mFileToLayout.get(file)); - } - - // Build up the DAG - for (File file : mIncludes.keySet()) { - Layout from = mFileToLayout.get(file); - assert from != null : file; - - List<String> includedLayouts = mIncludes.get(file); - for (String name : includedLayouts) { - Collection<Layout> layouts = nameToLayout.get(name); - if (layouts != null && !layouts.isEmpty()) { - if (layouts.size() == 1) { - from.include(layouts.iterator().next()); - } else { - // See if we have an obvious match - File folder = from.getFile().getParentFile(); - File candidate = new File(folder, name + DOT_XML); - Layout candidateLayout = mFileToLayout.get(candidate); - if (candidateLayout != null) { - from.include(candidateLayout); - } else if (mFileToIds.containsKey(candidate)) { - // We had an entry in mFileToIds, but not a layout: this - // means that the file exists, but had no includes or ids. - // This can't be a valid match: there is a layout that we know - // the include will pick, but it has no includes (to other layouts) - // and no ids, so no need to look at it - continue; - } else { - for (Layout to : layouts) { - // Decide if the two targets are compatible - if (isCompatible(from, to)) { - from.include(to); - } - } - } - } - } else { - // The layout is including some layout which has no ids or other includes - // so it's not relevant for a duplicate id search - continue; - } - } - } - } - - /** Determine whether two layouts are compatible. They are not if they (for example) - * specify conflicting qualifiers such as {@code -land} and {@code -port}. - * @param from the include from - * @param to the include to - * @return true if the two are compatible */ - boolean isCompatible(Layout from, Layout to) { - File fromFolder = from.mFile.getParentFile(); - File toFolder = to.mFile.getParentFile(); - if (fromFolder.equals(toFolder)) { - return true; - } - - String[] fromQualifiers = fromFolder.getName().split("-"); //$NON-NLS-1$ - String[] toQualifiers = toFolder.getName().split("-"); //$NON-NLS-1$ - - if (isPortrait(fromQualifiers) != isPortrait(toQualifiers)) { - return false; - } - - return true; - } - - private boolean isPortrait(String[] qualifiers) { - for (String qualifier : qualifiers) { - if (qualifier.equals("port")) { //$NON-NLS-1$ - return true; - } else if (qualifier.equals("land")) { //$NON-NLS-1$ - return false; - } - } - - return true; // it's the default - } - - public void check() { - // Visit the DAG, looking for conflicts - for (Layout layout : mFileToLayout.values()) { - if (!layout.isIncluded()) { // Only check from "root" nodes - Deque<Layout> stack = new ArrayDeque<Layout>(); - getIds(layout, stack, new HashSet<Layout>()); - } - } - } - - /** - * Computes the cumulative set of ids used in a given layout. We can't - * just depth-first-search the graph and check the set of ids - * encountered along the way, because we need to detect when multiple - * includes contribute the same ids. For example, if a file is included - * more than once, that would result in duplicates. - */ - private Set<String> getIds(Layout layout, Deque<Layout> stack, Set<Layout> seen) { - seen.add(layout); - - Set<String> layoutIds = layout.getIds(); - List<Layout> includes = layout.getIncludes(); - if (includes != null) { - Set<String> ids = new HashSet<String>(); - if (layoutIds != null) { - ids.addAll(layoutIds); - } - - stack.push(layout); - - Multimap<String, Set<String>> nameToIds = - ArrayListMultimap.create(includes.size(), 4); - - for (Layout included : includes) { - if (seen.contains(included)) { - continue; - } - Set<String> includedIds = getIds(included, stack, seen); - if (includedIds != null) { - String layoutName = included.getLayoutName(); - - idCheck: - for (String id : includedIds) { - if (ids.contains(id)) { - Collection<Set<String>> idSets = nameToIds.get(layoutName); - if (idSets != null) { - for (Set<String> siblingIds : idSets) { - if (siblingIds.contains(id)) { - // The id reference was added by a sibling, - // so no need to complain (again) - continue idCheck; - } - } - } - - // Duplicate! Record location request for new phase. - if (mLocations == null) { - mErrors = new ArrayList<Occurrence>(); - mLocations = ArrayListMultimap.create(); - mContext.getDriver().requestRepeat(DuplicateIdDetector.this, - Scope.ALL_RESOURCES_SCOPE); - } - - Map<Layout, Occurrence> occurrences = - new HashMap<Layout, Occurrence>(); - findId(layout, id, new ArrayDeque<Layout>(), occurrences, - new HashSet<Layout>()); - assert occurrences.size() >= 2; - - // Stash a request to find the given include - Collection<Occurrence> values = occurrences.values(); - List<Occurrence> sorted = new ArrayList<Occurrence>(values); - Collections.sort(sorted); - String msg = String.format( - "Duplicate id %1$s, defined or included multiple " + - "times in %2$s: %3$s", - id, layout.getDisplayName(), - sorted.toString()); - - // Store location request for the <include> tag - Occurrence primary = new Occurrence(layout.getFile(), msg, null); - Multimap<String, Occurrence> m = ArrayListMultimap.create(); - m.put(layoutName, primary); - mLocations.put(layout.getFile(), m); - mErrors.add(primary); - - Occurrence prev = primary; - - // Now store all the included occurrences of the id - for (Occurrence occurrence : values) { - if (occurrence.file.equals(layout.getFile())) { - occurrence.message = "Defined here"; - } else { - occurrence.message = String.format( - "Defined here, included via %1$s", - occurrence.includePath); - } - - m = ArrayListMultimap.create(); - m.put(id, occurrence); - mLocations.put(occurrence.file, m); - - // Link locations together - prev.next = occurrence; - prev = occurrence; - } - } - ids.add(id); - } - - // Store these ids such that on a conflict, we can tell when - // an id was added by a single variation of this file - nameToIds.put(layoutName, includedIds); - } - } - Layout visited = stack.pop(); - assert visited == layout; - return ids; - } else { - return layoutIds; - } - } - - private void findId(Layout layout, String id, Deque<Layout> stack, - Map<Layout, Occurrence> occurrences, Set<Layout> seen) { - seen.add(layout); - - Set<String> layoutIds = layout.getIds(); - if (layoutIds != null && layoutIds.contains(id)) { - StringBuilder path = new StringBuilder(80); - - if (!stack.isEmpty()) { - Iterator<Layout> iterator = stack.descendingIterator(); - while (iterator.hasNext()) { - path.append(iterator.next().getDisplayName()); - path.append(" => "); - } - } - path.append(layout.getDisplayName()); - path.append(" defines "); - path.append(id); - - assert occurrences.get(layout) == null : id + ',' + layout; - occurrences.put(layout, new Occurrence(layout.getFile(), null, path.toString())); - } - - List<Layout> includes = layout.getIncludes(); - if (includes != null) { - stack.push(layout); - for (Layout included : includes) { - if (!seen.contains(included)) { - findId(included, id, stack, occurrences, seen); - } - } - Layout visited = stack.pop(); - assert visited == layout; - } - } - } - - private static class Occurrence implements Comparable<Occurrence> { - public final File file; - public final String includePath; - public Occurrence next; - public Location location; - public String message; - - public Occurrence(File file, String message, String includePath) { - this.file = file; - this.message = message; - this.includePath = includePath; - } - - @Override - public String toString() { - return includePath != null ? includePath : message; - } - - @Override - public int compareTo(Occurrence other) { - // First sort by length, then sort by name - int delta = toString().length() - other.toString().length(); - if (delta != 0) { - return delta; - } - - return toString().compareTo(other.toString()); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateResourceDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateResourceDetector.java deleted file mode 100644 index 004303c..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/DuplicateResourceDetector.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright (C) 2012 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.ATTR_NAME; -import static com.android.SdkConstants.ATTR_TYPE; -import static com.android.SdkConstants.TAG_ITEM; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.resources.ResourceType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.android.utils.Pair; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.io.File; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * This detector identifies cases where a resource is defined multiple times in the - * same resource folder - */ -public class DuplicateResourceDetector extends ResourceXmlDetector { - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "DuplicateDefinition", //$NON-NLS-1$ - "Discovers duplicate definitions of resources", - - "You can define a resource multiple times in different resource folders; that's how " + - "string translations are done, for example. However, defining the same resource " + - "more than once in the same resource folder is likely an error, for example " + - "attempting to add a new resource without realizing that the name is already used, " + - "and so on.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - DuplicateResourceDetector.class, - Scope.ALL_RESOURCES_SCOPE).addAnalysisScope(Scope.RESOURCE_FILE_SCOPE); - - private static final String PRODUCT = "product"; //$NON-NLS-1$ - private Map<ResourceType, Set<String>> mTypeMap; - private Map<ResourceType, List<Pair<String, Location.Handle>>> mLocations; - private File mParent; - - /** Constructs a new {@link DuplicateResourceDetector} */ - public DuplicateResourceDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - @Override - @Nullable - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_NAME); - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - File parent = context.file.getParentFile(); - if (!parent.equals(mParent)) { - mParent = parent; - mTypeMap = Maps.newEnumMap(ResourceType.class); - mLocations = Maps.newEnumMap(ResourceType.class); - } - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - Element element = attribute.getOwnerElement(); - - if (element.hasAttribute(PRODUCT)) { - return; - } - - String tag = element.getTagName(); - String typeString = tag; - if (tag.equals(TAG_ITEM)) { - typeString = element.getAttribute(ATTR_TYPE); - } - ResourceType type = ResourceType.getEnum(typeString); - if (type == null) { - return; - } - - if (type == ResourceType.ATTR - && element.getParentNode().getNodeName().equals( - ResourceType.DECLARE_STYLEABLE.getName())) { - return; - } - - Set<String> names = mTypeMap.get(type); - if (names == null) { - names = Sets.newHashSetWithExpectedSize(40); - mTypeMap.put(type, names); - } - - String name = attribute.getValue(); - if (names.contains(name)) { - String message = String.format("%1$s has already been defined in this folder", - name); - Location location = context.getLocation(attribute); - List<Pair<String, Handle>> list = mLocations.get(type); - for (Pair<String, Handle> pair : list) { - if (name.equals(pair.getFirst())) { - Location secondary = pair.getSecond().resolve(); - secondary.setMessage("Previously defined here"); - location.setSecondary(secondary); - } - } - context.report(ISSUE, attribute, location, message, null); - } else { - names.add(name); - List<Pair<String, Handle>> list = mLocations.get(type); - if (list == null) { - list = Lists.newArrayList(); - mLocations.put(type, list); - } - Location.Handle handle = context.parser.createLocationHandle(context, attribute); - list.add(Pair.of(name, handle)); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ExtraTextDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ExtraTextDetector.java deleted file mode 100644 index e0781db..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ExtraTextDetector.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.DefaultPosition; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Position; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Document; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -/** - * Check which looks for invalid resources. Aapt already performs some validation, - * such as making sure that resource references point to resources that exist, but this - * detector looks for additional issues. - */ -public class ExtraTextDetector extends ResourceXmlDetector { - private boolean mFoundText; - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ExtraText", //$NON-NLS-1$ - "Looks for extraneous text in layout files", - "Layout resource files should only contain elements and attributes. Any XML " + - "text content found in the file is likely accidental (and potentially " + - "dangerous if the text resembles XML and the developer believes the text " + - "to be functional)", - Category.CORRECTNESS, - 3, - Severity.WARNING, - ExtraTextDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new detector */ - public ExtraTextDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT - || folderType == ResourceFolderType.MENU - || folderType == ResourceFolderType.ANIM - || folderType == ResourceFolderType.ANIMATOR - || folderType == ResourceFolderType.DRAWABLE - || folderType == ResourceFolderType.COLOR; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - mFoundText = false; - visitNode(context, document); - } - - private void visitNode(XmlContext context, Node node) { - short nodeType = node.getNodeType(); - if (nodeType == Node.TEXT_NODE && !mFoundText) { - String text = node.getNodeValue(); - for (int i = 0, n = text.length(); i < n; i++) { - char c = text.charAt(i); - if (!Character.isWhitespace(c)) { - String snippet = text.trim(); - int maxLength = 100; - if (snippet.length() > maxLength) { - snippet = snippet.substring(0, maxLength) + "..."; - } - Location location = context.getLocation(node); - if (i > 0) { - // Adjust the error position to point to the beginning of - // the text rather than the beginning of the text node - // (which is often the newline at the end of the previous - // line and the indentation) - Position start = location.getStart(); - if (start != null) { - int line = start.getLine(); - int column = start.getColumn(); - int offset = start.getOffset(); - - for (int j = 0; j < i; j++) { - offset++; - - if (text.charAt(j) == '\n') { - if (line != -1) { - line++; - } - if (column != -1) { - column = 0; - } - } else if (column != -1) { - column++; - } - } - - start = new DefaultPosition(line, column, offset); - location = Location.create(context.file, start, location.getEnd()); - } - } - context.report(ISSUE, node, location, - String.format("Unexpected text found in layout file: \"%1$s\"", - snippet), null); - mFoundText = true; - break; - } - } - } - - // Visit children - NodeList childNodes = node.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - visitNode(context, child); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FieldGetterDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FieldGetterDetector.java deleted file mode 100644 index 04841ab..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FieldGetterDetector.java +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -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.google.common.collect.Maps; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.VarInsnNode; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Looks for getter calls within the same class that could be replaced by - * direct field references instead. - */ -public class FieldGetterDetector extends Detector implements Detector.ClassScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "FieldGetter", //$NON-NLS-1$ - "Suggests replacing uses of getters with direct field access within a class", - - "Accessing a field within the class that defines a getter for that field is " + - "at least 3 times faster than calling the getter. For simple getters that do " + - "nothing other than return the field, you might want to just reference the " + - "local field directly instead.\n" + - "\n" + - "NOTE: As of Android 2.3 (Gingerbread), this optimization is performed " + - "automatically by Dalvik, so there is no need to change your code; this is " + - "only relevant if you are targeting older versions of Android.", - - Category.PERFORMANCE, - 4, - Severity.WARNING, - FieldGetterDetector.class, - Scope.CLASS_FILE_SCOPE). - // This is a micro-optimization: not enabled by default - setEnabledByDefault(false).setMoreInfo( - "http://developer.android.com/guide/practices/design/performance.html#internal_get_set"); //$NON-NLS-1$ - private ArrayList<Entry> mPendingCalls; - - /** Constructs a new {@link FieldGetterDetector} check */ - public FieldGetterDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - public int[] getApplicableAsmNodeTypes() { - return new int[] { AbstractInsnNode.METHOD_INSN }; - } - - @Override - public void checkInstruction(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull AbstractInsnNode instruction) { - // As of Gingerbread/API 9, Dalvik performs this optimization automatically - if (context.getProject().getMinSdk() >= 9) { - return; - } - - if ((method.access & Opcodes.ACC_STATIC) != 0) { - // Not an instance method - return; - } - - if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) { - return; - } - - MethodInsnNode node = (MethodInsnNode) instruction; - String name = node.name; - String owner = node.owner; - - AbstractInsnNode prev = LintUtils.getPrevInstruction(instruction); - if (prev == null || prev.getOpcode() != Opcodes.ALOAD) { - return; - } - VarInsnNode prevVar = (VarInsnNode) prev; - if (prevVar.var != 0) { // Not on "this", variable 0 in instance methods? - return; - } - - if (((name.startsWith("get") && name.length() > 3 //$NON-NLS-1$ - && Character.isUpperCase(name.charAt(3))) - || (name.startsWith("is") && name.length() > 2 //$NON-NLS-1$ - && Character.isUpperCase(name.charAt(2)))) - && owner.equals(classNode.name)) { - // Calling a potential getter method on self. We now need to - // investigate the method body of the getter call and make sure - // it's really a plain getter, not just a method which happens - // to have a method name like a getter, or a method which not - // only returns a field but possibly computes it or performs - // other initialization or side effects. This is done in a - // second pass over the bytecode, initiated by the finish() - // method. - if (mPendingCalls == null) { - mPendingCalls = new ArrayList<Entry>(); - } - - mPendingCalls.add(new Entry(name, node, method)); - } - - super.checkInstruction(context, classNode, method, instruction); - } - - @Override - public void afterCheckFile(@NonNull Context c) { - ClassContext context = (ClassContext) c; - - if (mPendingCalls != null) { - Set<String> names = new HashSet<String>(mPendingCalls.size()); - for (Entry entry : mPendingCalls) { - names.add(entry.name); - } - - Map<String, String> getters = checkMethods(context.getClassNode(), names); - if (!getters.isEmpty()) { - for (String getter : getters.keySet()) { - for (Entry entry : mPendingCalls) { - String name = entry.name; - // There can be more than one reference to the same name: - // one for each call site - if (name.equals(getter)) { - Location location = context.getLocation(entry.call); - String fieldName = getters.get(getter); - if (fieldName == null) { - fieldName = ""; - } - context.report(ISSUE, entry.method, entry.call, location, - String.format( - "Calling getter method %1$s() on self is " + - "slower than field access (%2$s)", getter, fieldName), fieldName); - } - } - } - } - } - - mPendingCalls = null; - } - - // Holder class for getters to be checked - private static class Entry { - public final String name; - public final MethodNode method; - public final MethodInsnNode call; - - public Entry(String name, MethodInsnNode call, MethodNode method) { - super(); - this.name = name; - this.call = call; - this.method = method; - } - } - - // Validate that these getter methods are really just simple field getters - // like these int and String getters: - // public int getFoo(); - // Code: - // 0: aload_0 - // 1: getfield #21; //Field mFoo:I - // 4: ireturn - // - // public java.lang.String getBar(); - // Code: - // 0: aload_0 - // 1: getfield #25; //Field mBar:Ljava/lang/String; - // 4: areturn - // - // Returns a map of valid getters as keys, and if the field name is found, the field name - // for each getter as its value. - private static Map<String, String> checkMethods(ClassNode classNode, Set<String> names) { - Map<String, String> validGetters = Maps.newHashMap(); - @SuppressWarnings("rawtypes") - List methods = classNode.methods; - String fieldName = null; - checkMethod: - for (Object methodObject : methods) { - MethodNode method = (MethodNode) methodObject; - if (names.contains(method.name) - && method.desc.startsWith("()")) { //$NON-NLS-1$ // (): No arguments - InsnList instructions = method.instructions; - int mState = 1; - for (AbstractInsnNode curr = instructions.getFirst(); - curr != null; - curr = curr.getNext()) { - switch (curr.getOpcode()) { - case -1: - // Skip label and line number nodes - continue; - case Opcodes.ALOAD: - if (mState == 1) { - fieldName = null; - mState = 2; - } else { - continue checkMethod; - } - break; - case Opcodes.GETFIELD: - if (mState == 2) { - FieldInsnNode field = (FieldInsnNode) curr; - fieldName = field.name; - mState = 3; - } else { - continue checkMethod; - } - break; - case Opcodes.ARETURN: - case Opcodes.FRETURN: - case Opcodes.IRETURN: - case Opcodes.DRETURN: - case Opcodes.LRETURN: - case Opcodes.RETURN: - if (mState == 3) { - validGetters.put(method.name, fieldName); - } - continue checkMethod; - default: - continue checkMethod; - } - } - } - } - - return validGetters; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FragmentDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FragmentDetector.java deleted file mode 100644 index f6ebcd6..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/FragmentDetector.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (C) 2012 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.CONSTRUCTOR_NAME; -import static com.android.SdkConstants.FRAGMENT; -import static com.android.SdkConstants.FRAGMENT_V4; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LintUtils; -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 org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.List; - -/** - * Checks that Fragment subclasses can be instantiated via - * {link {@link Class#newInstance()}}: the class is public, static, and has - * a public null constructor. - * <p> - * This helps track down issues like - * http://stackoverflow.com/questions/8058809/fragment-activity-crashes-on-screen-rotate - * (and countless duplicates) - */ -public class FragmentDetector extends Detector implements ClassScanner { - private static final String FRAGMENT_NAME_SUFFIX = "Fragment"; //$NON-NLS-1$ - - /** Are fragment subclasses instantiatable? */ - public static final Issue ISSUE = Issue.create( - "ValidFragment", //$NON-NLS-1$ - "Ensures that Fragment subclasses can be instantiated", - - "From the Fragment documentation:\n" + - "*Every* fragment must have an empty constructor, so it can be instantiated when " + - "restoring its activity's state. It is strongly recommended that subclasses do not " + - "have other constructors with parameters, since these constructors will not be " + - "called when the fragment is re-instantiated; instead, arguments can be supplied " + - "by the caller with `setArguments(Bundle)` and later retrieved by the Fragment " + - "with `getArguments()`.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - FragmentDetector.class, - Scope.CLASS_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/reference/android/app/Fragment.html#Fragment()"); //$NON-NLS-1$ - - - /** Constructs a new {@link FragmentDetector} */ - public FragmentDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) { - // Ignore abstract classes since they are clearly (and by definition) not intended to - // be instantiated. We're looking for accidental non-static or missing constructor - // scenarios here. - return; - } - - LintDriver driver = context.getDriver(); - - if (!(driver.isSubclassOf(classNode, FRAGMENT) - || driver.isSubclassOf(classNode, FRAGMENT_V4))) { - if (!context.getScope().contains(Scope.ALL_JAVA_FILES)) { - // Single file checking: Just check that it looks like a fragment class - // (since we don't have a full superclass map) - if (!classNode.name.endsWith(FRAGMENT_NAME_SUFFIX) || - classNode.superName == null) { - return; - } - } else { - return; - } - } - - if ((classNode.access & Opcodes.ACC_PUBLIC) == 0) { - context.report(ISSUE, context.getLocation(classNode), String.format( - "This fragment class should be public (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - return; - } - - if (classNode.name.indexOf('$') != -1 && !LintUtils.isStaticInnerClass(classNode)) { - context.report(ISSUE, context.getLocation(classNode), String.format( - "This fragment inner class should be static (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - return; - } - - boolean hasDefaultConstructor = false; - @SuppressWarnings("rawtypes") // ASM API - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - if (method.name.equals(CONSTRUCTOR_NAME)) { - if (method.desc.equals("()V")) { //$NON-NLS-1$ - // The constructor must be public - if ((method.access & Opcodes.ACC_PUBLIC) != 0) { - hasDefaultConstructor = true; - } else { - context.report(ISSUE, context.getLocation(method, classNode), - "The default constructor must be public", - null); - // Also mark that we have a constructor so we don't complain again - // below since we've already emitted a more specific error related - // to the default constructor - hasDefaultConstructor = true; - } - } else if (!method.desc.contains("()")) { //$NON-NLS-1$ - context.report(ISSUE, context.getLocation(method, classNode), - // TODO: Use separate issue for this which isn't an error - "Avoid non-default constructors in fragments: use a default constructor " + - "plus Fragment#setArguments(Bundle) instead", - null); - } - } - } - - if (!hasDefaultConstructor) { - context.report(ISSUE, context.getLocation(classNode), String.format( - "This fragment should provide a default constructor (a public " + - "constructor with no arguments) (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/GridLayoutDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/GridLayoutDetector.java deleted file mode 100644 index c348502..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/GridLayoutDetector.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_COLUMN_COUNT; -import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; -import static com.android.SdkConstants.ATTR_LAYOUT_ROW; -import static com.android.SdkConstants.ATTR_ROW_COUNT; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; - -/** - * Check which looks for potential errors in declarations of GridLayouts, such as specifying - * row/column numbers outside the declared dimensions of the grid. - */ -public class GridLayoutDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "GridLayout", //$NON-NLS-1$ - "Checks for potential GridLayout errors like declaring rows and columns outside " + - "the declared grid dimensions", - "Declaring a layout_row or layout_column that falls outside the declared size " + - "of a GridLayout's `rowCount` or `columnCount` is usually an unintentional error.", - Category.CORRECTNESS, - 4, - Severity.FATAL, - GridLayoutDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link GridLayoutDetector} check */ - public GridLayoutDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList( - "GridLayout" //$NON-NLS-1$ - ); - } - - private static int getInt(Element element, String attribute, int defaultValue) { - String valueString = element.getAttributeNS(ANDROID_URI, attribute); - if (valueString != null && !valueString.isEmpty()) { - try { - return Integer.decode(valueString); - } catch (NumberFormatException nufe) { - // Ignore - error in user's XML - } - } - - return defaultValue; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - int declaredRowCount = getInt(element, ATTR_ROW_COUNT, -1); - int declaredColumnCount = getInt(element, ATTR_COLUMN_COUNT, -1); - - if (declaredColumnCount != -1 || declaredRowCount != -1) { - for (Element child : LintUtils.getChildren(element)) { - if (declaredColumnCount != -1) { - int column = getInt(child, ATTR_LAYOUT_COLUMN, -1); - if (column >= declaredColumnCount) { - Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_COLUMN); - context.report(ISSUE, node, context.getLocation(node), - String.format("Column attribute (%1$d) exceeds declared grid column count (%2$d)", - column, declaredColumnCount), null); - } - } - if (declaredRowCount != -1) { - int row = getInt(child, ATTR_LAYOUT_ROW, -1); - if (row > declaredRowCount) { - Attr node = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_ROW); - context.report(ISSUE, node, context.getLocation(node), - String.format("Row attribute (%1$d) exceeds declared grid row count (%2$d)", - row, declaredRowCount), null); - } - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HandlerDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HandlerDetector.java deleted file mode 100644 index cfe8f0b..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HandlerDetector.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -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 org.objectweb.asm.tree.ClassNode; - -/** - * Checks that Handler implementations are top level classes or static. - * See the corresponding check in the android.os.Handler source code. - */ -public class HandlerDetector extends Detector implements ClassScanner { - - /** Potentially leaking handlers */ - public static final Issue ISSUE = Issue.create( - "HandlerLeak", //$NON-NLS-1$ - "Ensures that Handler classes do not hold on to a reference to an outer class", - - "In Android, Handler classes should be static or leaks might occur. " + - "Messages enqueued on the application thread's MessageQueue also retain their " + - "target Handler. If the Handler is an inner class, its outer class will be " + - "retained as well. To avoid leaking the outer class, declare the Handler as a " + - "static nested class with a WeakReference to its outer class.", - - Category.PERFORMANCE, - 4, - Severity.WARNING, - HandlerDetector.class, - Scope.CLASS_FILE_SCOPE); - - /** Constructs a new {@link HandlerDetector} */ - public HandlerDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if (classNode.name.indexOf('$') == -1) { - return; - } - - if (context.getDriver().isSubclassOf(classNode, "android/os/Handler") //$NON-NLS-1$ - && !LintUtils.isStaticInnerClass(classNode)) { - Location location = context.getLocation(classNode); - context.report(ISSUE, location, String.format( - "This Handler class should be static or leaks might occur (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedDebugModeDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedDebugModeDetector.java deleted file mode 100644 index fd678ca..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedDebugModeDetector.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_DEBUGGABLE; - -import com.android.annotations.NonNull; -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.Issue; -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.XmlContext; - -import org.w3c.dom.Attr; - -import java.io.File; -import java.util.Collection; -import java.util.Collections; - -/** - * Checks for hardcoded debug mode in manifest files - */ -public class HardcodedDebugModeDetector extends Detector implements Detector.XmlScanner { - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "HardcodedDebugMode", //$NON-NLS-1$ - "Checks for hardcoded values of android:debuggable in the manifest", - - "It's best to leave out the `android:debuggable` attribute from the manifest. " + - "If you do, then the tools will automatically insert `android:debuggable=true` when " + - "building an APK to debug on an emulator or device. And when you perform a " + - "release build, such as Exporting APK, it will automatically set it to `false`.\n" + - "\n" + - "If on the other hand you specify a specific value in the manifest file, then " + - "the tools will always use it. This can lead to accidentally publishing " + - "your app with debug information.", - - Category.SECURITY, - 5, - Severity.WARNING, - HardcodedDebugModeDetector.class, - Scope.MANIFEST_SCOPE); - - /** Constructs a new {@link HardcodedDebugModeDetector} check */ - public HardcodedDebugModeDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - // ---- Implements Detector.XmlScanner ---- - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singleton(ATTR_DEBUGGABLE); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (attribute.getNamespaceURI().equals(ANDROID_URI)) { - //if (attribute.getOwnerElement().getTagName().equals(TAG_APPLICATION)) { - context.report(ISSUE, attribute, context.getLocation(attribute), - "Avoid hardcoding the debug mode; leaving it out allows debug and " + - "release builds to automatically assign one", null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedValuesDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedValuesDetector.java deleted file mode 100644 index 11cc19d..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/HardcodedValuesDetector.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_CONTENT_DESCRIPTION; -import static com.android.SdkConstants.ATTR_HINT; -import static com.android.SdkConstants.ATTR_LABEL; -import static com.android.SdkConstants.ATTR_PROMPT; -import static com.android.SdkConstants.ATTR_TEXT; -import static com.android.SdkConstants.ATTR_TITLE; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; - -import java.util.Arrays; -import java.util.Collection; - -/** - * Check which looks at the children of ScrollViews and ensures that they fill/match - * the parent width instead of setting wrap_content. - */ -public class HardcodedValuesDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "HardcodedText", //$NON-NLS-1$ - "Looks for hardcoded text attributes which should be converted to resource lookup", - "Hardcoding text attributes directly in layout files is bad for several reasons:\n" + - "\n" + - "* When creating configuration variations (for example for landscape or portrait)" + - "you have to repeat the actual text (and keep it up to date when making changes)\n" + - "\n" + - "* The application cannot be translated to other languages by just adding new " + - "translations for existing string resources.\n" + - "\n" + - "In Eclipse there is a quickfix to automatically extract this hardcoded string into " + - "a resource lookup.", - - Category.I18N, - 5, - Severity.WARNING, - HardcodedValuesDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - // TODO: Add additional issues here, such as hardcoded colors, hardcoded sizes, etc - - /** Constructs a new {@link HardcodedValuesDetector} */ - public HardcodedValuesDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return Arrays.asList( - // Layouts - ATTR_TEXT, - ATTR_CONTENT_DESCRIPTION, - ATTR_HINT, - ATTR_LABEL, - ATTR_PROMPT, - - // Menus - ATTR_TITLE - ); - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.MENU; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String value = attribute.getValue(); - if (!value.isEmpty() && (value.charAt(0) != '@' && value.charAt(0) != '?')) { - // Make sure this is really one of the android: attributes - if (!ANDROID_URI.equals(attribute.getNamespaceURI())) { - return; - } - - context.report(ISSUE, attribute, context.getLocation(attribute), - String.format("[I18N] Hardcoded string \"%1$s\", should use @string resource", - value), null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/IconDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/IconDetector.java deleted file mode 100644 index 7f26c96..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/IconDetector.java +++ /dev/null @@ -1,1906 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_ICON; -import static com.android.SdkConstants.DOT_9PNG; -import static com.android.SdkConstants.DOT_GIF; -import static com.android.SdkConstants.DOT_JPEG; -import static com.android.SdkConstants.DOT_JPG; -import static com.android.SdkConstants.DOT_PNG; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.DRAWABLE_FOLDER; -import static com.android.SdkConstants.DRAWABLE_HDPI; -import static com.android.SdkConstants.DRAWABLE_LDPI; -import static com.android.SdkConstants.DRAWABLE_MDPI; -import static com.android.SdkConstants.DRAWABLE_PREFIX; -import static com.android.SdkConstants.DRAWABLE_XHDPI; -import static com.android.SdkConstants.MENU_TYPE; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.R_DRAWABLE_PREFIX; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_APPLICATION; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_PROVIDER; -import static com.android.SdkConstants.TAG_RECEIVER; -import static com.android.SdkConstants.TAG_SERVICE; -import static com.android.tools.lint.detector.api.LintUtils.endsWith; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Multimap; -import com.google.common.collect.Sets; - -import org.w3c.dom.Element; - -import java.awt.Dimension; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.regex.Pattern; - -import javax.imageio.ImageIO; -import javax.imageio.ImageReader; -import javax.imageio.stream.ImageInputStream; - -import lombok.ast.AstVisitor; -import lombok.ast.ConstructorInvocation; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.TypeReference; -import lombok.ast.TypeReferencePart; -import lombok.ast.VariableReference; - -/** - * Checks for common icon problems, such as wrong icon sizes, placing icons in the - * density independent drawable folder, etc. - */ -public class IconDetector extends ResourceXmlDetector implements Detector.JavaScanner { - - private static final boolean INCLUDE_LDPI; - static { - boolean includeLdpi = false; - - String value = System.getenv("ANDROID_LINT_INCLUDE_LDPI"); //$NON-NLS-1$ - if (value != null) { - includeLdpi = Boolean.valueOf(value); - } - INCLUDE_LDPI = includeLdpi; - } - - /** Pattern for the expected density folders to be found in the project */ - private static final Pattern DENSITY_PATTERN = Pattern.compile( - "^drawable-(nodpi|xhdpi|hdpi|mdpi" //$NON-NLS-1$ - + (INCLUDE_LDPI ? "|ldpi" : "") + ")$"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ - - private static final String[] REQUIRED_DENSITIES = INCLUDE_LDPI - ? new String[] { DRAWABLE_LDPI, DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI } - : new String[] { DRAWABLE_MDPI, DRAWABLE_HDPI, DRAWABLE_XHDPI }; - - private static final String[] DENSITY_QUALIFIERS = - new String[] { - "-ldpi", //$NON-NLS-1$ - "-mdpi", //$NON-NLS-1$ - "-hdpi", //$NON-NLS-1$ - "-xhdpi" //$NON-NLS-1$ - }; - - /** Scope needed to detect the types of icons (which involves scanning .java files, - * the manifest, menu files etc to see how icons are used - */ - private static final EnumSet<Scope> ICON_TYPE_SCOPE = EnumSet.of(Scope.ALL_RESOURCE_FILES, - Scope.JAVA_FILE, Scope.MANIFEST); - - /** Wrong icon size according to published conventions */ - public static final Issue ICON_EXPECTED_SIZE = Issue.create( - "IconExpectedSize", //$NON-NLS-1$ - "Ensures that launcher icons, notification icons etc have the correct size", - "There are predefined sizes (for each density) for launcher icons. You " + - "should follow these conventions to make sure your icons fit in with the " + - "overall look of the platform.", - Category.ICONS, - 5, - Severity.WARNING, - IconDetector.class, - ICON_TYPE_SCOPE) - // Still some potential false positives: - .setEnabledByDefault(false) - .setMoreInfo( - "http://developer.android.com/design/style/iconography.html"); //$NON-NLS-1$ - - /** Inconsistent dip size across densities */ - public static final Issue ICON_DIP_SIZE = Issue.create( - "IconDipSize", //$NON-NLS-1$ - "Ensures that icons across densities provide roughly the same density-independent size", - "Checks the all icons which are provided in multiple densities, all compute to " + - "roughly the same density-independent pixel (`dip`) size. This catches errors where " + - "images are either placed in the wrong folder, or icons are changed to new sizes " + - "but some folders are forgotten.", - Category.ICONS, - 5, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Images in res/drawable folder */ - public static final Issue ICON_LOCATION = Issue.create( - "IconLocation", //$NON-NLS-1$ - "Ensures that images are not defined in the density-independent drawable folder", - "The res/drawable folder is intended for density-independent graphics such as " + - "shapes defined in XML. For bitmaps, move it to `drawable-mdpi` and consider " + - "providing higher and lower resolution versions in `drawable-ldpi`, `drawable-hdpi` " + - "and `drawable-xhdpi`. If the icon *really* is density independent (for example " + - "a solid color) you can place it in `drawable-nodpi`.", - Category.ICONS, - 5, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$ - - /** Missing density versions of image */ - public static final Issue ICON_DENSITIES = Issue.create( - "IconDensities", //$NON-NLS-1$ - "Ensures that icons provide custom versions for all supported densities", - "Icons will look best if a custom version is provided for each of the " + - "major screen density classes (low, medium, high, extra high). " + - "This lint check identifies icons which do not have complete coverage " + - "across the densities.\n" + - "\n" + - "Low density is not really used much anymore, so this check ignores " + - "the ldpi density. To force lint to include it, set the environment " + - "variable `ANDROID_LINT_INCLUDE_LDPI=true`. For more information on " + - "current density usage, see " + - "http://developer.android.com/resources/dashboard/screens.html", - Category.ICONS, - 4, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$ - - /** Missing density folders */ - public static final Issue ICON_MISSING_FOLDER = Issue.create( - "IconMissingDensityFolder", //$NON-NLS-1$ - "Ensures that all the density folders are present", - "Icons will look best if a custom version is provided for each of the " + - "major screen density classes (low, medium, high, extra high). " + - "This lint check identifies folders which are missing, such as `drawable-hdpi`." + - "\n" + - "Low density is not really used much anymore, so this check ignores " + - "the ldpi density. To force lint to include it, set the environment " + - "variable `ANDROID_LINT_INCLUDE_LDPI=true`. For more information on " + - "current density usage, see " + - "http://developer.android.com/resources/dashboard/screens.html", - Category.ICONS, - 3, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/screens_support.html"); //$NON-NLS-1$ - - /** Using .gif bitmaps */ - public static final Issue GIF_USAGE = Issue.create( - "GifUsage", //$NON-NLS-1$ - "Checks for images using the GIF file format which is discouraged", - "The `.gif` file format is discouraged. Consider using `.png` (preferred) " + - "or `.jpg` (acceptable) instead.", - Category.ICONS, - 5, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/resources/drawable-resource.html#Bitmap"); //$NON-NLS-1$ - - /** Duplicated icons across different names */ - public static final Issue DUPLICATES_NAMES = Issue.create( - "IconDuplicates", //$NON-NLS-1$ - "Finds duplicated icons under different names", - "If an icon is repeated under different names, you can consolidate and just " + - "use one of the icons and delete the others to make your application smaller. " + - "However, duplicated icons usually are not intentional and can sometimes point " + - "to icons that were accidentally overwritten or accidentally not updated.", - Category.ICONS, - 3, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Duplicated contents across configurations for a given name */ - public static final Issue DUPLICATES_CONFIGURATIONS = Issue.create( - "IconDuplicatesConfig", //$NON-NLS-1$ - "Finds icons that have identical bitmaps across various configuration parameters", - "If an icon is provided under different configuration parameters such as " + - "`drawable-hdpi` or `-v11`, they should typically be different. This detector " + - "catches cases where the same icon is provided in different configuration folder " + - "which is usually not intentional.", - Category.ICONS, - 5, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Icons appearing in both -nodpi and a -Ndpi folder */ - public static final Issue ICON_NODPI = Issue.create( - "IconNoDpi", //$NON-NLS-1$ - "Finds icons that appear in both a -nodpi folder and a dpi folder", - "Bitmaps that appear in `drawable-nodpi` folders will not be scaled by the " + - "Android framework. If a drawable resource of the same name appears *both* in " + - "a `-nodpi` folder as well as a dpi folder such as `drawable-hdpi`, then " + - "the behavior is ambiguous and probably not intentional. Delete one or the " + - "other, or use different names for the icons.", - Category.ICONS, - 7, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Icons appearing as both drawable xml files and bitmaps */ - public static final Issue ICON_XML_AND_PNG = Issue.create( - "IconXmlAndPng", //$NON-NLS-1$ - "Finds icons that appear both as a drawable .xml file and as bitmaps", - "If a drawable resource appears as an .xml file in the drawable/ folder, " + - "it's usually not intentional for it to also appear as a bitmap using the " + - "same name; generally you expect the drawable XML file to define states " + - "and each state has a corresponding drawable bitmap.", - Category.ICONS, - 7, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Wrong filename according to the format */ - public static final Issue ICON_EXTENSION = Issue.create( - "IconExtension", //$NON-NLS-1$ - "Checks that the icon file extension matches the actual image format in the file", - - "Ensures that icons have the correct file extension (e.g. a .png file is " + - "really in the PNG format and not for example a GIF file named .png.)", - Category.ICONS, - 3, - Severity.WARNING, - IconDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Wrong filename according to the format */ - public static final Issue ICON_COLORS = Issue.create( - "IconColors", //$NON-NLS-1$ - "Checks that icons follow the recommended visual style", - - "Notification icons and Action Bar icons should only white and shades of gray. " + - "See the Android Design Guide for more details. " + - "Note that the way Lint decides whether an icon is an action bar icon or " + - "a notification icon is based on the filename prefix: `ic_menu_` for " + - "action bar icons, `ic_stat_` for notification icons etc. These correspond " + - "to the naming conventions documented in " + - "http://developer.android.com/guide/practices/ui_guidelines/icon_design.html", - Category.ICONS, - 6, - Severity.WARNING, - IconDetector.class, - ICON_TYPE_SCOPE).setMoreInfo( - "http://developer.android.com/design/style/iconography.html"); //$NON-NLS-1$ - - /** Wrong launcher icon shape */ - public static final Issue ICON_LAUNCHER_SHAPE = Issue.create( - "IconLauncherShape", //$NON-NLS-1$ - "Checks that launcher icons follow the recommended visual style", - - "According to the Android Design Guide " + - "(http://developer.android.com/design/style/iconography.html) " + - "your launcher icons should \"use a distinct silhouette\", " + - "a \"three-dimensional, front view, with a slight perspective as if viewed " + - "from above, so that users perceive some depth.\"\n" + - "\n" + - "The unique silhouette implies that your launcher icon should not be a filled " + - "square.", - Category.ICONS, - 6, - Severity.WARNING, - IconDetector.class, - ICON_TYPE_SCOPE).setMoreInfo( - "http://developer.android.com/design/style/iconography.html"); //$NON-NLS-1$ - - /** Constructs a new {@link IconDetector} check */ - public IconDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - mLauncherIcons = null; - mActionBarIcons = null; - mNotificationIcons = null; - } - - @Override - public void afterCheckLibraryProject(@NonNull Context context) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - checkResourceFolder(context, context.getProject()); - } - - @Override - public void afterCheckProject(@NonNull Context context) { - checkResourceFolder(context, context.getProject()); - } - - private void checkResourceFolder(Context context, @NonNull Project project) { - List<File> resourceFolders = project.getResourceFolders(); - for (File res : resourceFolders) { - File[] folders = res.listFiles(); - if (folders != null) { - boolean checkFolders = context.isEnabled(ICON_DENSITIES) - || context.isEnabled(ICON_MISSING_FOLDER) - || context.isEnabled(ICON_NODPI) - || context.isEnabled(ICON_XML_AND_PNG); - boolean checkDipSizes = context.isEnabled(ICON_DIP_SIZE); - boolean checkDuplicates = context.isEnabled(DUPLICATES_NAMES) - || context.isEnabled(DUPLICATES_CONFIGURATIONS); - - Map<File, Dimension> pixelSizes = null; - Map<File, Long> fileSizes = null; - if (checkDipSizes || checkDuplicates) { - pixelSizes = new HashMap<File, Dimension>(); - fileSizes = new HashMap<File, Long>(); - } - Map<File, Set<String>> folderToNames = new HashMap<File, Set<String>>(); - Map<File, Set<String>> nonDpiFolderNames = new HashMap<File, Set<String>>(); - for (File folder : folders) { - String folderName = folder.getName(); - if (folderName.startsWith(DRAWABLE_FOLDER)) { - File[] files = folder.listFiles(); - if (files != null) { - checkDrawableDir(context, folder, files, pixelSizes, fileSizes); - - if (checkFolders && DENSITY_PATTERN.matcher(folderName).matches()) { - Set<String> names = new HashSet<String>(files.length); - for (File f : files) { - String name = f.getName(); - if (isDrawableFile(name)) { - names.add(name); - } - } - folderToNames.put(folder, names); - } else if (checkFolders) { - Set<String> names = new HashSet<String>(files.length); - for (File f : files) { - String name = f.getName(); - if (isDrawableFile(name)) { - names.add(name); - } - } - nonDpiFolderNames.put(folder, names); - } - } - } - } - - if (checkDipSizes) { - checkDipSizes(context, pixelSizes); - } - - if (checkDuplicates) { - checkDuplicates(context, pixelSizes, fileSizes); - } - - if (checkFolders && !folderToNames.isEmpty()) { - checkDensities(context, res, folderToNames, nonDpiFolderNames); - } - } - } - } - - private static boolean isDrawableFile(String name) { - // endsWith(name, DOT_PNG) is also true for endsWith(name, DOT_9PNG) - return endsWith(name, DOT_PNG)|| endsWith(name, DOT_JPG) || endsWith(name, DOT_GIF) - || endsWith(name, DOT_XML) || endsWith(name, DOT_JPEG); - } - - // This method looks for duplicates in the assets. This uses two pieces of information - // (file sizes and image dimensions) to quickly reject candidates, such that it only - // needs to check actual file contents on a small subset of the available files. - private static void checkDuplicates(Context context, Map<File, Dimension> pixelSizes, - Map<File, Long> fileSizes) { - Map<Long, Set<File>> sameSizes = new HashMap<Long, Set<File>>(); - Map<Long, File> seenSizes = new HashMap<Long, File>(fileSizes.size()); - for (Map.Entry<File, Long> entry : fileSizes.entrySet()) { - File file = entry.getKey(); - Long size = entry.getValue(); - if (seenSizes.containsKey(size)) { - Set<File> set = sameSizes.get(size); - if (set == null) { - set = new HashSet<File>(); - set.add(seenSizes.get(size)); - sameSizes.put(size, set); - } - set.add(file); - } else { - seenSizes.put(size, file); - } - } - - if (sameSizes.isEmpty()) { - return; - } - - // Now go through the files that have the same size and check to see if we can - // split them apart based on image dimensions - // Note: we may not have file sizes on all the icons; in particular, - // we don't have file sizes for ninepatch files. - Collection<Set<File>> candidateLists = sameSizes.values(); - for (Set<File> candidates : candidateLists) { - Map<Dimension, Set<File>> sameDimensions = new HashMap<Dimension, Set<File>>( - candidates.size()); - List<File> noSize = new ArrayList<File>(); - for (File file : candidates) { - Dimension dimension = pixelSizes.get(file); - if (dimension != null) { - Set<File> set = sameDimensions.get(dimension); - if (set == null) { - set = new HashSet<File>(); - sameDimensions.put(dimension, set); - } - set.add(file); - } else { - noSize.add(file); - } - } - - - // Files that we have no dimensions for must be compared against everything - Collection<Set<File>> sets = sameDimensions.values(); - if (!noSize.isEmpty()) { - if (!sets.isEmpty()) { - for (Set<File> set : sets) { - set.addAll(noSize); - } - } else { - // Must just test the noSize elements against themselves - HashSet<File> noSizeSet = new HashSet<File>(noSize); - sets = Collections.<Set<File>>singletonList(noSizeSet); - } - } - - // Map from file to actual byte contents of the file. - // We store this in a map such that for repeated files, such as noSize files - // which can appear in multiple buckets, we only need to read them once - Map<File, byte[]> fileContents = new HashMap<File, byte[]>(); - - // Now we're ready for the final check where we actually check the - // bits. We have to partition the files into buckets of files that - // are identical. - for (Set<File> set : sets) { - if (set.size() < 2) { - continue; - } - - // Read all files in this set and store in map - for (File file : set) { - byte[] bits = fileContents.get(file); - if (bits == null) { - try { - bits = context.getClient().readBytes(file); - fileContents.put(file, bits); - } catch (IOException e) { - context.log(e, null); - } - } - } - - // Map where the key file is known to be equal to the value file. - // After we check individual files for equality this will be used - // to look for transitive equality. - Map<File, File> equal = new HashMap<File, File>(); - - // Now go and compare all the files. This isn't an efficient algorithm - // but the number of candidates should be very small - - List<File> files = new ArrayList<File>(set); - Collections.sort(files); - for (int i = 0; i < files.size() - 1; i++) { - for (int j = i + 1; j < files.size(); j++) { - File file1 = files.get(i); - File file2 = files.get(j); - byte[] contents1 = fileContents.get(file1); - byte[] contents2 = fileContents.get(file2); - if (contents1 == null || contents2 == null) { - // File couldn't be read: ignore - continue; - } - if (contents1.length != contents2.length) { - // Sizes differ: not identical. - // This shouldn't happen since we've already partitioned based - // on File.length(), but just make sure here since the file - // system could have lied, or cached a value that has changed - // if the file was just overwritten - continue; - } - boolean same = true; - for (int k = 0; k < contents1.length; k++) { - if (contents1[k] != contents2[k]) { - same = false; - break; - } - } - if (same) { - equal.put(file1, file2); - } - } - } - - if (!equal.isEmpty()) { - Map<File, Set<File>> partitions = new HashMap<File, Set<File>>(); - List<Set<File>> sameSets = new ArrayList<Set<File>>(); - for (Map.Entry<File, File> entry : equal.entrySet()) { - File file1 = entry.getKey(); - File file2 = entry.getValue(); - Set<File> set1 = partitions.get(file1); - Set<File> set2 = partitions.get(file2); - if (set1 != null) { - set1.add(file2); - } else if (set2 != null) { - set2.add(file1); - } else { - set = new HashSet<File>(); - sameSets.add(set); - set.add(file1); - set.add(file2); - partitions.put(file1, set); - partitions.put(file2, set); - } - } - - // We've computed the partitions of equal files. Now sort them - // for stable output. - List<List<File>> lists = new ArrayList<List<File>>(); - for (Set<File> same : sameSets) { - assert !same.isEmpty(); - ArrayList<File> sorted = new ArrayList<File>(same); - Collections.sort(sorted); - lists.add(sorted); - } - // Sort overall partitions by the first item in each list - Collections.sort(lists, new Comparator<List<File>>() { - @Override - public int compare(List<File> list1, List<File> list2) { - return list1.get(0).compareTo(list2.get(0)); - } - }); - - for (List<File> sameFiles : lists) { - Location location = null; - boolean sameNames = true; - String lastName = null; - for (File file : sameFiles) { - if (lastName != null && !lastName.equals(file.getName())) { - sameNames = false; - } - lastName = file.getName(); - // Chain locations together - Location linkedLocation = location; - location = Location.create(file); - location.setSecondary(linkedLocation); - } - - if (sameNames) { - StringBuilder sb = new StringBuilder(sameFiles.size() * 16); - for (File file : sameFiles) { - if (sb.length() > 0) { - sb.append(", "); //$NON-NLS-1$ - } - sb.append(file.getParentFile().getName()); - } - String message = String.format( - "The %1$s icon has identical contents in the following configuration folders: %2$s", - lastName, sb.toString()); - context.report(DUPLICATES_CONFIGURATIONS, location, message, null); - } else { - StringBuilder sb = new StringBuilder(sameFiles.size() * 16); - for (File file : sameFiles) { - if (sb.length() > 0) { - sb.append(", "); //$NON-NLS-1$ - } - sb.append(file.getName()); - } - String message = String.format( - "The following unrelated icon files have identical contents: %1$s", - sb.toString()); - context.report(DUPLICATES_NAMES, location, message, null); - } - } - } - } - } - - } - - // This method checks the given map from resource file to pixel dimensions for each - // such image and makes sure that the normalized dip sizes across all the densities - // are mostly the same. - private static void checkDipSizes(Context context, Map<File, Dimension> pixelSizes) { - // Partition up the files such that I can look at a series by name. This - // creates a map from filename (such as foo.png) to a list of files - // providing that icon in various folders: drawable-mdpi/foo.png, drawable-hdpi/foo.png - // etc. - Map<String, List<File>> nameToFiles = new HashMap<String, List<File>>(); - for (File file : pixelSizes.keySet()) { - String name = file.getName(); - List<File> list = nameToFiles.get(name); - if (list == null) { - list = new ArrayList<File>(); - nameToFiles.put(name, list); - } - list.add(file); - } - - ArrayList<String> names = new ArrayList<String>(nameToFiles.keySet()); - Collections.sort(names); - - // We have to partition the files further because it's possible for the project - // to have different configurations for an icon, such as this: - // drawable-large-hdpi/foo.png, drawable-large-mdpi/foo.png, - // drawable-hdpi/foo.png, drawable-mdpi/foo.png, - // drawable-hdpi-v11/foo.png and drawable-mdpi-v11/foo.png. - // In this case we don't want to compare across categories; we want to - // ensure that the drawable-large-{density} icons are consistent, - // that the drawable-{density}-v11 icons are consistent, and that - // the drawable-{density} icons are consistent. - - // Map from name to list of map from parent folder to list of files - Map<String, Map<String, List<File>>> configMap = - new HashMap<String, Map<String,List<File>>>(); - for (Map.Entry<String, List<File>> entry : nameToFiles.entrySet()) { - String name = entry.getKey(); - List<File> files = entry.getValue(); - for (File file : files) { - String parentName = file.getParentFile().getName(); - // Strip out the density part - int index = -1; - for (String qualifier : DENSITY_QUALIFIERS) { - index = parentName.indexOf(qualifier); - if (index != -1) { - parentName = parentName.substring(0, index) - + parentName.substring(index + qualifier.length()); - break; - } - } - if (index == -1) { - // No relevant qualifier found in the parent directory name, - // e.g. it's just "drawable" or something like "drawable-nodpi". - continue; - } - - Map<String, List<File>> folderMap = configMap.get(name); - if (folderMap == null) { - folderMap = new HashMap<String,List<File>>(); - configMap.put(name, folderMap); - } - // Map from name to a map from parent folder to files - List<File> list = folderMap.get(parentName); - if (list == null) { - list = new ArrayList<File>(); - folderMap.put(parentName, list); - } - list.add(file); - } - } - - for (String name : names) { - //List<File> files = nameToFiles.get(name); - Map<String, List<File>> configurations = configMap.get(name); - if (configurations == null) { - // Nothing in this configuration: probably only found in drawable/ or - // drawable-nodpi etc directories. - continue; - } - - for (Map.Entry<String, List<File>> entry : configurations.entrySet()) { - List<File> files = entry.getValue(); - - // Ensure that all the dip sizes are *roughly* the same - Map<File, Dimension> dipSizes = new HashMap<File, Dimension>(); - int dipWidthSum = 0; // Incremental computation of average - int dipHeightSum = 0; // Incremental computation of average - int count = 0; - for (File file : files) { - float factor = getMdpiScalingFactor(file.getParentFile().getName()); - if (factor > 0) { - Dimension size = pixelSizes.get(file); - if (size == null) { - continue; - } - Dimension dip = new Dimension( - Math.round(size.width / factor), - Math.round(size.height / factor)); - dipWidthSum += dip.width; - dipHeightSum += dip.height; - dipSizes.put(file, dip); - count++; - } - } - if (count == 0) { - // Icons in drawable/ and drawable-nodpi/ - continue; - } - int meanWidth = dipWidthSum / count; - int meanHeight = dipHeightSum / count; - - // Compute standard deviation? - int squareWidthSum = 0; - int squareHeightSum = 0; - for (Dimension size : dipSizes.values()) { - squareWidthSum += (size.width - meanWidth) * (size.width - meanWidth); - squareHeightSum += (size.height - meanHeight) * (size.height - meanHeight); - } - double widthStdDev = Math.sqrt(squareWidthSum / count); - double heightStdDev = Math.sqrt(squareHeightSum / count); - - if (widthStdDev > meanWidth / 10 || heightStdDev > meanHeight) { - Location location = null; - StringBuilder sb = new StringBuilder(100); - - // Sort entries by decreasing dip size - List<Map.Entry<File, Dimension>> entries = - new ArrayList<Map.Entry<File,Dimension>>(); - for (Map.Entry<File, Dimension> entry2 : dipSizes.entrySet()) { - entries.add(entry2); - } - Collections.sort(entries, - new Comparator<Map.Entry<File, Dimension>>() { - @Override - public int compare(Entry<File, Dimension> e1, - Entry<File, Dimension> e2) { - Dimension d1 = e1.getValue(); - Dimension d2 = e2.getValue(); - if (d1.width != d2.width) { - return d2.width - d1.width; - } - - return d2.height - d1.height; - } - }); - for (Map.Entry<File, Dimension> entry2 : entries) { - if (sb.length() > 0) { - sb.append(", "); - } - File file = entry2.getKey(); - - // Chain locations together - Location linkedLocation = location; - location = Location.create(file); - location.setSecondary(linkedLocation); - Dimension dip = entry2.getValue(); - Dimension px = pixelSizes.get(file); - String fileName = file.getParentFile().getName() + File.separator - + file.getName(); - sb.append(String.format("%1$s: %2$dx%3$d dp (%4$dx%5$d px)", - fileName, dip.width, dip.height, px.width, px.height)); - } - String message = String.format( - "The image %1$s varies significantly in its density-independent (dip) " + - "size across the various density versions: %2$s", - name, sb.toString()); - context.report(ICON_DIP_SIZE, location, message, null); - } - } - } - } - - private static void checkDensities(Context context, File res, - Map<File, Set<String>> folderToNames, - Map<File, Set<String>> nonDpiFolderNames) { - // TODO: Is there a way to look at the manifest and figure out whether - // all densities are expected to be needed? - // Note: ldpi is probably not needed; it has very little usage - // (about 2%; http://developer.android.com/resources/dashboard/screens.html) - // TODO: Use the matrix to check out if we can eliminate densities based - // on the target screens? - - Set<String> definedDensities = new HashSet<String>(); - for (File f : folderToNames.keySet()) { - definedDensities.add(f.getName()); - } - - // Look for missing folders -- if you define say drawable-mdpi then you - // should also define -hdpi and -xhdpi. - if (context.isEnabled(ICON_MISSING_FOLDER)) { - List<String> missing = new ArrayList<String>(); - for (String density : REQUIRED_DENSITIES) { - if (!definedDensities.contains(density)) { - missing.add(density); - } - } - if (!missing.isEmpty()) { - context.report( - ICON_MISSING_FOLDER, - Location.create(res), - String.format("Missing density variation folders in %1$s: %2$s", - context.getProject().getDisplayPath(res), - LintUtils.formatList(missing, -1)), - null); - } - } - - if (context.isEnabled(ICON_NODPI)) { - Set<String> noDpiNames = new HashSet<String>(); - for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { - if (isNoDpiFolder(entry.getKey())) { - noDpiNames.addAll(entry.getValue()); - } - } - if (!noDpiNames.isEmpty()) { - // Make sure that none of the nodpi names appear in a non-nodpi folder - Set<String> inBoth = new HashSet<String>(); - List<File> files = new ArrayList<File>(); - for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { - File folder = entry.getKey(); - String folderName = folder.getName(); - if (!isNoDpiFolder(folder)) { - assert DENSITY_PATTERN.matcher(folderName).matches(); - Set<String> overlap = nameIntersection(noDpiNames, entry.getValue()); - inBoth.addAll(overlap); - for (String name : overlap) { - files.add(new File(folder, name)); - } - } - } - - if (!inBoth.isEmpty()) { - List<String> list = new ArrayList<String>(inBoth); - Collections.sort(list); - - // Chain locations together - Collections.sort(files); - Location location = null; - for (File file : files) { - Location linkedLocation = location; - location = Location.create(file); - location.setSecondary(linkedLocation); - } - - context.report(ICON_NODPI, location, - String.format( - "The following images appear in both -nodpi and in a density folder: %1$s", - LintUtils.formatList(list, - context.getDriver().isAbbreviating() ? 10 : -1)), - null); - } - } - } - - if (context.isEnabled(ICON_XML_AND_PNG)) { - Map<File, Set<String>> folderMap = Maps.newHashMap(folderToNames); - folderMap.putAll(nonDpiFolderNames); - Set<String> xmlNames = Sets.newHashSetWithExpectedSize(100); - Set<String> bitmapNames = Sets.newHashSetWithExpectedSize(100); - - for (Map.Entry<File, Set<String>> entry : folderMap.entrySet()) { - Set<String> names = entry.getValue(); - for (String name : names) { - if (endsWith(name, DOT_XML)) { - xmlNames.add(name); - } else if (isDrawableFile(name)) { - bitmapNames.add(name); - } - } - } - if (!xmlNames.isEmpty() && !bitmapNames.isEmpty()) { - // Make sure that none of the nodpi names appear in a non-nodpi folder - Set<String> overlap = nameIntersection(xmlNames, bitmapNames); - if (!overlap.isEmpty()) { - Multimap<String, File> map = ArrayListMultimap.create(); - Set<String> bases = Sets.newHashSetWithExpectedSize(overlap.size()); - for (String name : overlap) { - bases.add(LintUtils.getBaseName(name)); - } - - for (String base : bases) { - for (Map.Entry<File, Set<String>> entry : folderMap.entrySet()) { - File folder = entry.getKey(); - for (String n : entry.getValue()) { - if (base.equals(LintUtils.getBaseName(n))) { - map.put(base, new File(folder, n)); - } - } - } - } - List<String> sorted = new ArrayList<String>(map.keySet()); - Collections.sort(sorted); - for (String name : sorted) { - List<File> lists = Lists.newArrayList(map.get(name)); - Collections.sort(lists); - - // Chain locations together - Location location = null; - for (File file : lists) { - Location linkedLocation = location; - location = Location.create(file); - location.setSecondary(linkedLocation); - } - - List<String> fileNames = Lists.newArrayList(); - boolean seenXml = false; - boolean seenNonXml = false; - for (File f : lists) { - boolean isXml = endsWith(f.getPath(), DOT_XML); - if (isXml && !seenXml) { - fileNames.add(context.getProject().getDisplayPath(f)); - seenXml = true; - } else if (!isXml && !seenNonXml) { - fileNames.add(context.getProject().getDisplayPath(f)); - seenNonXml = true; - } - } - - context.report(ICON_XML_AND_PNG, location, - String.format( - "The following images appear both as density independent .xml files and as bitmap files: %1$s", - LintUtils.formatList(fileNames, - context.getDriver().isAbbreviating() ? 10 : -1)), - null); - } - } - } - } - - if (context.isEnabled(ICON_DENSITIES)) { - // Look for folders missing some of the specific assets - Set<String> allNames = new HashSet<String>(); - for (Entry<File,Set<String>> entry : folderToNames.entrySet()) { - if (!isNoDpiFolder(entry.getKey())) { - Set<String> names = entry.getValue(); - allNames.addAll(names); - } - } - - for (Map.Entry<File, Set<String>> entry : folderToNames.entrySet()) { - File file = entry.getKey(); - if (isNoDpiFolder(file)) { - continue; - } - Set<String> names = entry.getValue(); - if (names.size() != allNames.size()) { - List<String> delta = new ArrayList<String>(nameDifferences(allNames, names)); - if (delta.isEmpty()) { - continue; - } - Collections.sort(delta); - String foundIn = ""; - if (delta.size() == 1) { - // Produce list of where the icon is actually defined - List<String> defined = new ArrayList<String>(); - String name = delta.get(0); - for (Map.Entry<File, Set<String>> e : folderToNames.entrySet()) { - if (e.getValue().contains(name)) { - defined.add(e.getKey().getName()); - } - } - if (!defined.isEmpty()) { - foundIn = String.format(" (found in %1$s)", - LintUtils.formatList(defined, - context.getDriver().isAbbreviating() ? 5 : -1)); - } - } - - context.report(ICON_DENSITIES, Location.create(file), - String.format( - "Missing the following drawables in %1$s: %2$s%3$s", - file.getName(), - LintUtils.formatList(delta, - context.getDriver().isAbbreviating() ? 5 : -1), - foundIn), - null); - } - } - } - } - - /** - * Compute the difference in names between a and b. This is not just - * Sets.difference(a, b) because we want to make the comparisons <b>without - * file extensions</b> and return the result <b>with</b>.. - */ - private static Set<String> nameDifferences(Set<String> a, Set<String> b) { - Set<String> names1 = new HashSet<String>(a.size()); - for (String s : a) { - names1.add(LintUtils.getBaseName(s)); - } - Set<String> names2 = new HashSet<String>(b.size()); - for (String s : b) { - names2.add(LintUtils.getBaseName(s)); - } - - names1.removeAll(names2); - - if (!names1.isEmpty()) { - // Map filenames back to original filenames with extensions - Set<String> result = new HashSet<String>(names1.size()); - for (String s : a) { - if (names1.contains(LintUtils.getBaseName(s))) { - result.add(s); - } - } - for (String s : b) { - if (names1.contains(LintUtils.getBaseName(s))) { - result.add(s); - } - } - - return result; - } - - return Collections.emptySet(); - } - - /** - * Compute the intersection in names between a and b. This is not just - * Sets.intersection(a, b) because we want to make the comparisons <b>without - * file extensions</b> and return the result <b>with</b>. - */ - private static Set<String> nameIntersection(Set<String> a, Set<String> b) { - Set<String> names1 = new HashSet<String>(a.size()); - for (String s : a) { - names1.add(LintUtils.getBaseName(s)); - } - Set<String> names2 = new HashSet<String>(b.size()); - for (String s : b) { - names2.add(LintUtils.getBaseName(s)); - } - - names1.retainAll(names2); - - if (!names1.isEmpty()) { - // Map filenames back to original filenames with extensions - Set<String> result = new HashSet<String>(names1.size()); - for (String s : a) { - if (names1.contains(LintUtils.getBaseName(s))) { - result.add(s); - } - } - for (String s : b) { - if (names1.contains(LintUtils.getBaseName(s))) { - result.add(s); - } - } - - return result; - } - - return Collections.emptySet(); - } - - private static boolean isNoDpiFolder(File file) { - return file.getName().contains("-nodpi"); - } - - private Map<File, BufferedImage> mImageCache; - - @Nullable - private BufferedImage getImage(@Nullable File file) throws IOException { - if (mImageCache == null) { - mImageCache = Maps.newHashMap(); - } else { - BufferedImage image = mImageCache.get(file); - if (image != null) { - return image; - } - } - - BufferedImage image = ImageIO.read(file); - mImageCache.put(file, image); - - return image; - } - - private void checkDrawableDir(Context context, File folder, File[] files, - Map<File, Dimension> pixelSizes, Map<File, Long> fileSizes) { - if (folder.getName().equals(DRAWABLE_FOLDER) - && context.isEnabled(ICON_LOCATION) && - // If supporting older versions than Android 1.6, it's not an error - // to include bitmaps in drawable/ - context.getProject().getMinSdk() >= 4) { - for (File file : files) { - String name = file.getName(); - if (name.endsWith(DOT_XML)) { - // pass - most common case, avoids checking other extensions - } else if (endsWith(name, DOT_PNG) - || endsWith(name, DOT_JPG) - || endsWith(name, DOT_JPEG) - || endsWith(name, DOT_GIF)) { - context.report(ICON_LOCATION, - Location.create(file), - String.format("Found bitmap drawable res/drawable/%1$s in " + - "densityless folder", - file.getName()), - null); - } - } - } - - if (context.isEnabled(GIF_USAGE)) { - for (File file : files) { - String name = file.getName(); - if (endsWith(name, DOT_GIF)) { - context.report(GIF_USAGE, Location.create(file), - "Using the .gif format for bitmaps is discouraged", - null); - } - } - } - - if (context.isEnabled(ICON_EXTENSION)) { - for (File file : files) { - String path = file.getPath(); - if (isDrawableFile(path) && !endsWith(path, DOT_XML)) { - checkExtension(context, file); - } - } - } - - if (context.isEnabled(ICON_COLORS)) { - for (File file : files) { - String name = file.getName(); - - if (isDrawableFile(name) - && !endsWith(name, DOT_XML) - && !endsWith(name, DOT_9PNG)) { - String baseName = getBaseName(name); - boolean isActionBarIcon = isActionBarIcon(context, baseName, file); - if (isActionBarIcon || isNotificationIcon(baseName)) { - Dimension size = checkColor(context, file, isActionBarIcon); - - // Store dimension for size check if we went to the trouble of reading image - if (size != null && pixelSizes != null) { - pixelSizes.put(file, size); - } - } - } - } - } - - if (context.isEnabled(ICON_LAUNCHER_SHAPE)) { - // Look up launcher icon name - for (File file : files) { - String name = file.getName(); - if (isLauncherIcon(getBaseName(name)) - && !endsWith(name, DOT_XML) - && !endsWith(name, DOT_9PNG)) { - checkLauncherShape(context, file); - } - } - } - - // Check icon sizes - if (context.isEnabled(ICON_EXPECTED_SIZE)) { - checkExpectedSizes(context, folder, files); - } - - if (pixelSizes != null || fileSizes != null) { - for (File file : files) { - // TODO: Combine this check with the check for expected sizes such that - // I don't check file sizes twice! - String fileName = file.getName(); - - if (endsWith(fileName, DOT_PNG) || endsWith(fileName, DOT_JPG) - || endsWith(fileName, DOT_JPEG)) { - // Only scan .png files (except 9-patch png's) and jpg files for - // dip sizes. Duplicate checks can also be performed on ninepatch files. - if (pixelSizes != null && !endsWith(fileName, DOT_9PNG) - && !pixelSizes.containsKey(file)) { // already read by checkColor? - Dimension size = getSize(file); - pixelSizes.put(file, size); - } - if (fileSizes != null) { - fileSizes.put(file, file.length()); - } - } - } - } - - mImageCache = null; - } - - /** - * Check that launcher icons do not fill every pixel in the image - */ - private void checkLauncherShape(Context context, File file) { - try { - BufferedImage image = getImage(file); - if (image != null) { - // TODO: see if the shape is rectangular but inset from outer rectangle; if so - // that's probably not right either! - for (int y = 0, height = image.getHeight(); y < height; y++) { - for (int x = 0, width = image.getWidth(); x < width; x++) { - int rgb = image.getRGB(x, y); - if ((rgb & 0xFF000000) == 0) { - return; - } - } - } - - String message = "Launcher icons should not fill every pixel of their square " + - "region; see the design guide for details"; - context.report(ICON_LAUNCHER_SHAPE, Location.create(file), - message, null); - } - } catch (IOException e) { - // Pass: ignore files we can't read - } - } - - /** - * Check whether the icons in the file are okay. Also return the image size - * if known (for use by other checks) - */ - private Dimension checkColor(Context context, File file, boolean isActionBarIcon) { - int folderVersion = Context.getFolderVersion(file); - if (isActionBarIcon) { - if (folderVersion != -1 && folderVersion < 11 - || !isAndroid30(context, folderVersion)) { - return null; - } - } else { - if (folderVersion != -1 && folderVersion < 9 - || !isAndroid23(context, folderVersion) - && !isAndroid30(context, folderVersion)) { - return null; - } - } - - // TODO: This only checks icons that are known to be using the Holo style. - // However, if the user has minSdk < 11 as well as targetSdk > 11, we should - // also check that they actually include a -v11 or -v14 folder with proper - // icons, since the below won't flag the older icons. - try { - BufferedImage image = getImage(file); - if (image != null) { - if (isActionBarIcon) { - checkPixels: - for (int y = 0, height = image.getHeight(); y < height; y++) { - for (int x = 0, width = image.getWidth(); x < width; x++) { - int rgb = image.getRGB(x, y); - if ((rgb & 0xFF000000) != 0) { // else: transparent - int r = (rgb & 0xFF0000) >>> 16; - int g = (rgb & 0x00FF00) >>> 8; - int b = (rgb & 0x0000FF); - if (r != g || r != b) { - String message = "Action Bar icons should use a single gray " - + "color (#333333 for light themes (with 60%/30% " - + "opacity for enabled/disabled), and #FFFFFF with " - + "opacity 80%/30% for dark themes"; - context.report(ICON_COLORS, Location.create(file), - message, null); - break checkPixels; - } - } - } - } - } else { - if (folderVersion >= 11 || isAndroid30(context, folderVersion)) { - // Notification icons. Should be white as of API 14 - checkPixels: - for (int y = 0, height = image.getHeight(); y < height; y++) { - for (int x = 0, width = image.getWidth(); x < width; x++) { - int rgb = image.getRGB(x, y); - if ((rgb & 0xFF000000) != 0 - && rgb != 0xFFFFFFFF) { - int r = (rgb & 0xFF0000) >>> 16; - int g = (rgb & 0x00FF00) >>> 8; - int b = (rgb & 0x0000FF); - if (r == g && r == b) { - // If the pixel is not white, it might be because of - // anti-aliasing. In that case, at least one neighbor - // should be of a different color - if (x < width - 1 && rgb != image.getRGB(x + 1, y)) { - continue; - } - if (x > 0 && rgb != image.getRGB(x - 1, y)) { - continue; - } - if (y < height - 1 && rgb != image.getRGB(x, y + 1)) { - continue; - } - if (y > 0 && rgb != image.getRGB(x, y - 1)) { - continue; - } - } - - String message = "Notification icons must be entirely white"; - context.report(ICON_COLORS, Location.create(file), - message, null); - break checkPixels; - } - } - } - } else { - // As of API 9, should be gray. - checkPixels: - for (int y = 0, height = image.getHeight(); y < height; y++) { - for (int x = 0, width = image.getWidth(); x < width; x++) { - int rgb = image.getRGB(x, y); - if ((rgb & 0xFF000000) != 0) { // else: transparent - int r = (rgb & 0xFF0000) >>> 16; - int g = (rgb & 0x00FF00) >>> 8; - int b = (rgb & 0x0000FF); - if (r != g || r != b) { - String message = "Notification icons should not use " - + "colors"; - context.report(ICON_COLORS, Location.create(file), - message, null); - break checkPixels; - } - } - } - } - } - } - - return new Dimension(image.getWidth(), image.getHeight()); - } - } catch (IOException e) { - // Pass: ignore files we can't read - } - - return null; - } - - private static void checkExtension(Context context, File file) { - try { - ImageInputStream input = ImageIO.createImageInputStream(file); - if (input != null) { - try { - Iterator<ImageReader> readers = ImageIO.getImageReaders(input); - while (readers.hasNext()) { - ImageReader reader = readers.next(); - try { - reader.setInput(input); - - // Check file extension - String formatName = reader.getFormatName(); - if (formatName != null && !formatName.isEmpty()) { - String path = file.getPath(); - int index = path.lastIndexOf('.'); - String extension = path.substring(index+1).toLowerCase(Locale.US); - - if (!formatName.equalsIgnoreCase(extension)) { - if (endsWith(path, DOT_JPG) - && formatName.equals("JPEG")) { //$NON-NLS-1$ - return; - } - String message = String.format( - "Misleading file extension; named .%1$s but the " + - "file format is %2$s", extension, formatName); - Location location = Location.create(file); - context.report(ICON_EXTENSION, location, message, null); - } - break; - } - } finally { - reader.dispose(); - } - } - } finally { - if (input != null) { - input.close(); - } - } - } - } catch (IOException e) { - // Pass -- we can't handle all image types, warn about those we can - } - } - - private static String getBaseName(String name) { - String baseName = name; - int index = baseName.indexOf('.'); - if (index != -1) { - baseName = baseName.substring(0, index); - } - - return baseName; - } - - private void checkExpectedSizes(Context context, File folder, File[] files) { - if (files == null || files.length == 0) { - return; - } - - String folderName = folder.getName(); - int folderVersion = Context.getFolderVersion(files[0]); - - for (File file : files) { - String name = file.getName(); - - // TODO: Look up exact app icon from the manifest rather than simply relying on - // the naming conventions described here: - // http://developer.android.com/guide/practices/ui_guidelines/icon_design.html#design-tips - // See if we can figure out other types of icons from usage too. - - String baseName = getBaseName(name); - - if (isLauncherIcon(baseName)) { - // Launcher icons - checkSize(context, folderName, file, 48, 48, true /*exact*/); - } else if (isActionBarIcon(baseName)) { - checkSize(context, folderName, file, 32, 32, true /*exact*/); - } else if (name.startsWith("ic_dialog_")) { //$NON-NLS-1$ - // Dialog - checkSize(context, folderName, file, 32, 32, true /*exact*/); - } else if (name.startsWith("ic_tab_")) { //$NON-NLS-1$ - // Tab icons - checkSize(context, folderName, file, 32, 32, true /*exact*/); - } else if (isNotificationIcon(baseName)) { - // Notification icons - if (isAndroid30(context, folderVersion)) { - checkSize(context, folderName, file, 24, 24, true /*exact*/); - } else if (isAndroid23(context, folderVersion)) { - checkSize(context, folderName, file, 16, 25, false /*exact*/); - } else { - // Android 2.2 or earlier - // TODO: Should this be done for each folder size? - checkSize(context, folderName, file, 25, 25, true /*exact*/); - } - } else if (name.startsWith("ic_menu_")) { //$NON-NLS-1$ - if (isAndroid30(context, folderVersion)) { - // Menu icons (<=2.3 only: Replaced by action bar icons (ic_action_ in 3.0). - // However the table halfway down the page on - // http://developer.android.com/guide/practices/ui_guidelines/icon_design.html - // and the README in the icon template download says that convention is ic_menu - checkSize(context, folderName, file, 32, 32, true); - } else if (isAndroid23(context, folderVersion)) { - // The icon should be 32x32 inside the transparent image; should - // we check that this is mostly the case (a few pixels are allowed to - // overlap for anti-aliasing etc) - checkSize(context, folderName, file, 48, 48, true /*exact*/); - } else { - // Android 2.2 or earlier - // TODO: Should this be done for each folder size? - checkSize(context, folderName, file, 48, 48, true /*exact*/); - } - } - // TODO: ListView icons? - } - } - - /** - * Is this drawable folder for an Android 3.0 drawable? This will be the - * case if it specifies -v11+, or if the minimum SDK version declared in the - * manifest is at least 11. - */ - private static boolean isAndroid30(Context context, int folderVersion) { - return folderVersion >= 11 || context.getMainProject().getMinSdk() >= 11; - } - - /** - * Is this drawable folder for an Android 2.3 drawable? This will be the - * case if it specifies -v9 or -v10, or if the minimum SDK version declared in the - * manifest is 9 or 10 (and it does not specify some higher version like -v11 - */ - private static boolean isAndroid23(Context context, int folderVersion) { - if (isAndroid30(context, folderVersion)) { - return false; - } - - if (folderVersion == 9 || folderVersion == 10) { - return true; - } - - int minSdk = context.getMainProject().getMinSdk(); - - return minSdk == 9 || minSdk == 10; - } - - private static float getMdpiScalingFactor(String folderName) { - // Can't do startsWith(DRAWABLE_MDPI) because the folder could - // be something like "drawable-sw600dp-mdpi". - if (folderName.contains("-mdpi")) { //$NON-NLS-1$ - return 1.0f; - } else if (folderName.contains("-hdpi")) { //$NON-NLS-1$ - return 1.5f; - } else if (folderName.contains("-xhdpi")) { //$NON-NLS-1$ - return 2.0f; - } else if (folderName.contains("-ldpi")) { //$NON-NLS-1$ - return 0.75f; - } else { - return 0f; - } - } - - private static void checkSize(Context context, String folderName, File file, - int mdpiWidth, int mdpiHeight, boolean exactMatch) { - String fileName = file.getName(); - // Only scan .png files (except 9-patch png's) and jpg files - if (!((endsWith(fileName, DOT_PNG) && !endsWith(fileName, DOT_9PNG)) || - endsWith(fileName, DOT_JPG) || endsWith(fileName, DOT_JPEG))) { - return; - } - - int width; - int height; - // Use 3:4:6:8 scaling ratio to look up the other expected sizes - if (folderName.startsWith(DRAWABLE_MDPI)) { - width = mdpiWidth; - height = mdpiHeight; - } else if (folderName.startsWith(DRAWABLE_HDPI)) { - // Perform math using floating point; if we just do - // width = mdpiWidth * 3 / 2; - // then for mdpiWidth = 25 (as in notification icons on pre-GB) we end up - // with width = 37, instead of 38 (with floating point rounding we get 37.5 = 38) - width = Math.round(mdpiWidth * 3.f / 2); - height = Math.round(mdpiHeight * 3f / 2); - } else if (folderName.startsWith(DRAWABLE_XHDPI)) { - width = mdpiWidth * 2; - height = mdpiHeight * 2; - } else if (folderName.startsWith(DRAWABLE_LDPI)) { - width = Math.round(mdpiWidth * 3f / 4); - height = Math.round(mdpiHeight * 3f / 4); - } else { - return; - } - - Dimension size = getSize(file); - if (size != null) { - if (exactMatch && size.width != width || size.height != height) { - context.report( - ICON_EXPECTED_SIZE, - Location.create(file), - String.format( - "Incorrect icon size for %1$s: expected %2$dx%3$d, but was %4$dx%5$d", - folderName + File.separator + file.getName(), - width, height, size.width, size.height), - null); - } else if (!exactMatch && size.width > width || size.height > height) { - context.report( - ICON_EXPECTED_SIZE, - Location.create(file), - String.format( - "Incorrect icon size for %1$s: icon size should be at most %2$dx%3$d, but was %4$dx%5$d", - folderName + File.separator + file.getName(), - width, height, size.width, size.height), - null); - } - } - } - - private static Dimension getSize(File file) { - try { - ImageInputStream input = ImageIO.createImageInputStream(file); - if (input != null) { - try { - Iterator<ImageReader> readers = ImageIO.getImageReaders(input); - if (readers.hasNext()) { - ImageReader reader = readers.next(); - try { - reader.setInput(input); - return new Dimension(reader.getWidth(0), reader.getHeight(0)); - } finally { - reader.dispose(); - } - } - } finally { - if (input != null) { - input.close(); - } - } - } - - // Fallback: read the image using the normal means - BufferedImage image = ImageIO.read(file); - if (image != null) { - return new Dimension(image.getWidth(), image.getHeight()); - } else { - return null; - } - } catch (IOException e) { - // Pass -- we can't handle all image types, warn about those we can - return null; - } - } - - - private Set<String> mActionBarIcons; - private Set<String> mNotificationIcons; - private Set<String> mLauncherIcons; - private Multimap<String, String> mMenuToIcons; - - private boolean isLauncherIcon(String name) { - assert name.indexOf('.') == -1; // Should supply base name - - // Naming convention - if (name.startsWith("ic_launcher")) { //$NON-NLS-1$ - return true; - } - return mLauncherIcons != null && mLauncherIcons.contains(name); - } - - private boolean isNotificationIcon(String name) { - assert name.indexOf('.') == -1; // Should supply base name - - // Naming convention - if (name.startsWith("ic_stat_")) { //$NON-NLS-1$ - return true; - } - - return mNotificationIcons != null && mNotificationIcons.contains(name); - } - - private boolean isActionBarIcon(String name) { - assert name.indexOf('.') == -1; // Should supply base name - - // Naming convention - if (name.startsWith("ic_action_")) { //$NON-NLS-1$ - return true; - } - - // Naming convention - - return mActionBarIcons != null && mActionBarIcons.contains(name); - } - - private boolean isActionBarIcon(Context context, String name, File file) { - if (isActionBarIcon(name)) { - return true; - } - - // As of Android 3.0 ic_menu_ are action icons - if (file != null && name.startsWith("ic_menu_") //$NON-NLS-1$ - && isAndroid30(context, Context.getFolderVersion(file))) { - // Naming convention - return true; - } - - return false; - } - - // XML detector: Skim manifest and menu files - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.MENU; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - // Manifest - TAG_APPLICATION, - TAG_ACTIVITY, - TAG_SERVICE, - TAG_PROVIDER, - TAG_RECEIVER, - - // Menu - TAG_ITEM - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String icon = element.getAttributeNS(ANDROID_URI, ATTR_ICON); - if (icon != null && icon.startsWith(DRAWABLE_PREFIX)) { - icon = icon.substring(DRAWABLE_PREFIX.length()); - - String tagName = element.getTagName(); - if (tagName.equals(TAG_ITEM)) { - if (mMenuToIcons == null) { - mMenuToIcons = ArrayListMultimap.create(); - } - String menu = getBaseName(context.file.getName()); - mMenuToIcons.put(menu, icon); - } else { - // Manifest tags: launcher icons - if (mLauncherIcons == null) { - mLauncherIcons = Sets.newHashSet(); - } - mLauncherIcons.add(icon); - } - } - } - - - // ---- Implements JavaScanner ---- - - private static final String NOTIFICATION_CLASS = "Notification"; //$NON-NLS-1$ - private static final String NOTIFICATION_COMPAT_CLASS = "NotificationCompat"; //$NON-NLS-1$ - private static final String BUILDER_CLASS = "Builder"; //$NON-NLS-1$ - private static final String SET_SMALL_ICON = "setSmallIcon"; //$NON-NLS-1$ - private static final String ON_CREATE_OPTIONS_MENU = "onCreateOptionsMenu"; //$NON-NLS-1$ - - @Override - @Nullable - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new NotificationFinder(); - } - - @Override - @Nullable - public List<Class<? extends Node>> getApplicableNodeTypes() { - List<Class<? extends Node>> types = new ArrayList<Class<? extends Node>>(3); - types.add(MethodDeclaration.class); - types.add(ConstructorInvocation.class); - return types; - } - - private final class NotificationFinder extends ForwardingAstVisitor { - @Override - public boolean visitMethodDeclaration(MethodDeclaration node) { - if (ON_CREATE_OPTIONS_MENU.equals(node.astMethodName().astValue())) { - // Gather any R.menu references found in this method - node.accept(new MenuFinder()); - } - - return super.visitMethodDeclaration(node); - } - - @Override - public boolean visitConstructorInvocation(ConstructorInvocation node) { - TypeReference reference = node.astTypeReference(); - StrictListAccessor<TypeReferencePart, TypeReference> parts = reference.astParts(); - String typeName = parts.last().astIdentifier().astValue(); - if (NOTIFICATION_CLASS.equals(typeName)) { - StrictListAccessor<Expression, ConstructorInvocation> args = node.astArguments(); - if (args.size() == 3) { - if (args.first() instanceof Select && handleSelect((Select) args.first())) { - return super.visitConstructorInvocation(node); - } - - Node method = StringFormatDetector.getParentMethod(node); - if (method != null) { - // Must track local types - String name = StringFormatDetector.getResourceForFirstArg(method, node); - if (name != null) { - if (mNotificationIcons == null) { - mNotificationIcons = Sets.newHashSet(); - } - mNotificationIcons.add(name); - } - } - } - } else if (BUILDER_CLASS.equals(typeName)) { - boolean isBuilder = false; - if (parts.size() == 1) { - isBuilder = true; - } else if (parts.size() == 2) { - String clz = parts.first().astIdentifier().astValue(); - if (NOTIFICATION_CLASS.equals(clz) || NOTIFICATION_COMPAT_CLASS.equals(clz)) { - isBuilder = true; - } - } - if (isBuilder) { - Node method = StringFormatDetector.getParentMethod(node); - if (method != null) { - SetIconFinder finder = new SetIconFinder(); - method.accept(finder); - } - } - } - - return super.visitConstructorInvocation(node); - } - } - - private boolean handleSelect(Select select) { - if (select.toString().startsWith(R_DRAWABLE_PREFIX)) { - String name = select.astIdentifier().astValue(); - if (mNotificationIcons == null) { - mNotificationIcons = Sets.newHashSet(); - } - mNotificationIcons.add(name); - - return true; - } - - return false; - } - - private final class SetIconFinder extends ForwardingAstVisitor { - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (SET_SMALL_ICON.equals(node.astName().astValue())) { - StrictListAccessor<Expression,MethodInvocation> arguments = node.astArguments(); - if (arguments.size() == 1 && arguments.first() instanceof Select) { - handleSelect((Select) arguments.first()); - } - } - return super.visitMethodInvocation(node); - } - } - - private final class MenuFinder extends ForwardingAstVisitor { - @Override - public boolean visitSelect(Select node) { - // R.type.name - if (node.astOperand() instanceof Select) { - Select select = (Select) node.astOperand(); - if (select.astOperand() instanceof VariableReference) { - VariableReference reference = (VariableReference) select.astOperand(); - if (reference.astIdentifier().astValue().equals(R_CLASS)) { - String type = select.astIdentifier().astValue(); - - if (type.equals(MENU_TYPE)) { - String name = node.astIdentifier().astValue(); - // Reclassify icons in the given menu as action bar icons - if (mMenuToIcons != null) { - Collection<String> icons = mMenuToIcons.get(name); - if (icons != null) { - if (mActionBarIcons == null) { - mActionBarIcons = Sets.newHashSet(); - } - mActionBarIcons.addAll(icons); - } - } - } - } - } - } - - return super.visitSelect(node); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InefficientWeightDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InefficientWeightDetector.java deleted file mode 100644 index 0608de1..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InefficientWeightDetector.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BASELINE_ALIGNED; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.ATTR_ORIENTATION; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.RADIO_GROUP; -import static com.android.SdkConstants.VALUE_FILL_PARENT; -import static com.android.SdkConstants.VALUE_MATCH_PARENT; -import static com.android.SdkConstants.VALUE_VERTICAL; -import static com.android.SdkConstants.VIEW; -import static com.android.SdkConstants.VIEW_FRAGMENT; -import static com.android.SdkConstants.VIEW_INCLUDE; -import static com.android.SdkConstants.VIEW_TAG; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.SdkInfo; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.Collection; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Checks whether a layout_weight is declared inefficiently. - */ -public class InefficientWeightDetector extends LayoutDetector { - - /** Can a weight be replaced with 0dp instead for better performance? */ - public static final Issue INEFFICIENT_WEIGHT = Issue.create( - "InefficientWeight", //$NON-NLS-1$ - "Looks for inefficient weight declarations in LinearLayouts", - "When only a single widget in a LinearLayout defines a weight, it is more " + - "efficient to assign a width/height of `0dp` to it since it will absorb all " + - "the remaining space anyway. With a declared width/height of `0dp` it " + - "does not have to measure its own size first.", - Category.PERFORMANCE, - 3, - Severity.WARNING, - InefficientWeightDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Are weights nested? */ - public static final Issue NESTED_WEIGHTS = Issue.create( - "NestedWeights", //$NON-NLS-1$ - "Looks for nested layout weights, which are costly", - "Layout weights require a widget to be measured twice. When a LinearLayout with " + - "non-zero weights is nested inside another LinearLayout with non-zero weights, " + - "then the number of measurements increase exponentially.", - Category.PERFORMANCE, - 3, - Severity.WARNING, - InefficientWeightDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Should a LinearLayout set android:baselineAligned? */ - public static final Issue BASELINE_WEIGHTS = Issue.create( - "DisableBaselineAlignment", //$NON-NLS-1$ - "Looks for LinearLayouts which should set android:baselineAligned=false", - "When a LinearLayout is used to distribute the space proportionally between " + - "nested layouts, the baseline alignment property should be turned off to " + - "make the layout computation faster.", - Category.PERFORMANCE, - 3, - Severity.WARNING, - InefficientWeightDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Using 0dp on the wrong dimension */ - public static final Issue WRONG_0DP = Issue.create( - "Suspicious0dp", //$NON-NLS-1$ - "Looks for 0dp as the width in a vertical LinearLayout or as the height in a " + - "horizontal", - - "Using 0dp as the width in a horizontal LinearLayout with weights is a useful " + - "trick to ensure that only the weights (and not the intrinsic sizes) are used " + - "when sizing the children.\n" + - "\n" + - "However, if you use 0dp for the opposite dimension, the view will be invisible. " + - "This can happen if you change the orientation of a layout without also flipping " + - "the 0dp dimension in all the children.", - Category.CORRECTNESS, - 6, - Severity.ERROR, - InefficientWeightDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Missing explicit orientation */ - public static final Issue ORIENTATION = Issue.create( - "Orientation", //$NON-NLS-1$ - "Checks that LinearLayouts with multiple children set the orientation", - - "The default orientation of a LinearLayout is horizontal. It's pretty easy to " - + "believe that the layout is vertical, add multiple children to it, and wonder " - + "why only the first child is visible (when the subsequent children are " - + "off screen to the right). This lint rule helps pinpoint this issue by " - + "warning whenever a LinearLayout is used with an implicit orientation " - + "and multiple children.", - - Category.CORRECTNESS, - 2, - Severity.ERROR, - InefficientWeightDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** - * Map from element to whether that element has a non-zero linear layout - * weight or has an ancestor which does - */ - private final Map<Node, Boolean> mInsideWeight = new IdentityHashMap<Node, Boolean>(); - - /** Constructs a new {@link InefficientWeightDetector} */ - public InefficientWeightDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(LINEAR_LAYOUT); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - List<Element> children = LintUtils.getChildren(element); - // See if there is exactly one child with a weight - boolean multipleWeights = false; - Element weightChild = null; - boolean checkNesting = context.isEnabled(NESTED_WEIGHTS); - for (Element child : children) { - if (child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) { - if (weightChild != null) { - // More than one child defining a weight! - multipleWeights = true; - } else if (!multipleWeights) { - weightChild = child; - } - - if (checkNesting) { - mInsideWeight.put(child, Boolean.TRUE); - - Boolean inside = mInsideWeight.get(element); - if (inside == null) { - mInsideWeight.put(element, Boolean.FALSE); - } else if (inside) { - Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); - context.report(NESTED_WEIGHTS, sizeNode, - context.getLocation(sizeNode), - "Nested weights are bad for performance", null); - // Don't warn again - checkNesting = false; - } - } - } - } - - String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); - if (children.size() >= 2 && (orientation == null || orientation.isEmpty()) - && context.isEnabled(ORIENTATION)) { - // See if at least one of the children, except the last one, sets layout_width - // to match_parent (or fill_parent), in an implicitly horizontal layout, since - // that might mean the last child won't be visible. This is a source of confusion - // for new Android developers. - boolean maxWidthSet = false; - Iterator<Element> iterator = children.iterator(); - while (iterator.hasNext()) { - Element child = iterator.next(); - if (!iterator.hasNext()) { // Don't check the last one - break; - } - String width = child.getAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); - if (VALUE_MATCH_PARENT.equals(width) || VALUE_FILL_PARENT.equals(width)) { - // Also check that weights are not set here; this affects the computation - // a bit and the child may not fill up the whole linear layout - if (!child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) { - maxWidthSet = true; - break; - } - } - } - if (maxWidthSet && !element.hasAttribute(ATTR_STYLE)) { - String message = "Wrong orientation? No orientation specified, and the default " - + "is horizontal, yet this layout has multiple children where at " - + "least one has layout_width=\"match_parent\""; - context.report(ORIENTATION, element, context.getLocation(element), message, null); - } - } - - if (context.isEnabled(BASELINE_WEIGHTS) && weightChild != null - && !VALUE_VERTICAL.equals(orientation) - && !element.hasAttributeNS(ANDROID_URI, ATTR_BASELINE_ALIGNED)) { - // See if all the children are layouts - boolean allChildrenAreLayouts = !children.isEmpty(); - SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getProject()); - for (Element child : children) { - String tagName = child.getTagName(); - if (!(sdkInfo.isLayout(tagName) - // RadioGroup is a layout, but one which possibly should be base aligned - && !tagName.equals(RADIO_GROUP) - // Consider <fragment> tags as layouts for the purposes of this check - || VIEW_FRAGMENT.equals(tagName) - // Ditto for <include> tags - || VIEW_INCLUDE.equals(tagName))) { - allChildrenAreLayouts = false; - } - } - if (allChildrenAreLayouts) { - context.report(BASELINE_WEIGHTS, - element, - context.getLocation(element), - "Set android:baselineAligned=\"false\" on this element for better performance", - null); - } - } - - if (context.isEnabled(INEFFICIENT_WEIGHT) - && weightChild != null && !multipleWeights) { - String dimension; - if (VALUE_VERTICAL.equals(orientation)) { - dimension = ATTR_LAYOUT_HEIGHT; - } else { - dimension = ATTR_LAYOUT_WIDTH; - } - Attr sizeNode = weightChild.getAttributeNodeNS(ANDROID_URI, dimension); - String size = sizeNode != null ? sizeNode.getValue() : "(undefined)"; - if (!size.startsWith("0")) { //$NON-NLS-1$ - String msg = String.format( - "Use a %1$s of 0dip instead of %2$s for better performance", - dimension, size); - context.report(INEFFICIENT_WEIGHT, - weightChild, - context.getLocation(sizeNode != null ? sizeNode : weightChild), msg, null); - - } - } - - if (context.isEnabled(WRONG_0DP)) { - checkWrong0Dp(context, element, children); - } - } - - private static void checkWrong0Dp(XmlContext context, Element element, - List<Element> children) { - boolean isVertical = false; - String orientation = element.getAttributeNS(ANDROID_URI, ATTR_ORIENTATION); - if (VALUE_VERTICAL.equals(orientation)) { - isVertical = true; - } - - for (Element child : children) { - String tagName = child.getTagName(); - if (tagName.equals(VIEW)) { - // Might just used for spacing - return; - } - if (tagName.indexOf('.') != -1 || tagName.equals(VIEW_TAG)) { - // Custom views might perform their own dynamic sizing or ignore the layout - // attributes all together - return; - } - - boolean hasWeight = child.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT); - - Attr widthNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); - Attr heightNode = child.getAttributeNodeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); - - boolean noWidth = false; - boolean noHeight = false; - if (widthNode != null && widthNode.getValue().startsWith("0")) { //$NON-NLS-1$ - noWidth = true; - } - if (heightNode != null && heightNode.getValue().startsWith("0")) { //$NON-NLS-1$ - noHeight = true; - } else if (!noWidth) { - return; - } - - // If you're specifying 0dp for both the width and height you are probably - // trying to hide it deliberately - if (noWidth && noHeight) { - return; - } - assert noWidth || noHeight; - - if (noWidth) { - assert widthNode != null; - if (!hasWeight) { - context.report(WRONG_0DP, widthNode, context.getLocation(widthNode), - "Suspicious size: this will make the view invisible, should be " + - "used with layout_weight", null); - } else if (isVertical) { - context.report(WRONG_0DP, widthNode, context.getLocation(widthNode), - "Suspicious size: this will make the view invisible, probably " + - "intended for layout_height", null); - } - } else { - assert noHeight; - assert heightNode != null; - if (!hasWeight) { - context.report(WRONG_0DP, widthNode, context.getLocation(heightNode), - "Suspicious size: this will make the view invisible, should be " + - "used with layout_weight", null); - } else if (!isVertical) { - context.report(WRONG_0DP, widthNode, context.getLocation(heightNode), - "Suspicious size: this will make the view invisible, probably " + - "intended for layout_width", null); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java deleted file mode 100644 index 048f6ae..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/InvalidPackageDetector.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -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.Sets; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.FieldInsnNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.io.File; -import java.util.EnumSet; -import java.util.List; -import java.util.Set; - -import lombok.ast.libs.org.parboiled.google.collect.Lists; - -/** - * Looks for usages of Java packages that are not included in Android. - */ -public class InvalidPackageDetector extends Detector implements Detector.ClassScanner { - /** Accessing an invalid package */ - public static final Issue ISSUE = Issue.create("InvalidPackage", //$NON-NLS-1$ - "Finds API accesses to APIs that are not supported in Android", - - "This check scans through libraries looking for calls to APIs that are not included " + - "in Android.\n" + - "\n" + - "When you create Android projects, the classpath is set up such that you can only " + - "access classes in the API packages that are included in Android. However, if you " + - "add other projects to your libs/ folder, there is no guarantee that those .jar " + - "files were built with an Android specific classpath, and in particular, they " + - "could be accessing unsupported APIs such as java.applet.\n" + - "\n" + - "This check scans through library jars and looks for references to API packages " + - "that are not included in Android and flags these. This is only an error if your " + - "code calls one of the library classes which wind up referencing the unsupported " + - "package.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - InvalidPackageDetector.class, - EnumSet.of(Scope.JAVA_LIBRARIES)); - - private static final String JAVA_PKG_PREFIX = "java/"; //$NON-NLS-1$ - private static final String JAVAX_PKG_PREFIX = "javax/"; //$NON-NLS-1$ - - private ApiLookup mApiDatabase; - - /** - * List of candidates that are potential package violations. These are - * recorded as candidates rather than flagged immediately such that we can - * filter out hits for classes that are also defined as libraries (possibly - * encountered later in the library traversal). - */ - private List<Candidate> mCandidates; - /** - * Set of Java packages defined in the libraries; this means that if the - * user has added libraries in this package namespace (such as the - * null annotations jars) we don't flag these. - */ - private final Set<String> mJavaxLibraryClasses = Sets.newHashSetWithExpectedSize(64); - - /** Constructs a new package check */ - public InvalidPackageDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - mApiDatabase = ApiLookup.get(context.getClient()); - } - - // ---- Implements ClassScanner ---- - - @SuppressWarnings("rawtypes") // ASM API - @Override - public void checkClass(@NonNull final ClassContext context, @NonNull ClassNode classNode) { - if (!context.isFromClassLibrary() || shouldSkip(context.file)) { - return; - } - - if (mApiDatabase == null) { - return; - } - - if (classNode.name.startsWith(JAVAX_PKG_PREFIX)) { - mJavaxLibraryClasses.add(classNode.name); - } - - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - - InsnList nodes = method.instructions; - - // Check return type - // The parameter types are already handled as local variables so we can skip - // right to the return type. - // Check types in parameter list - String signature = method.desc; - if (signature != null) { - int args = signature.indexOf(')'); - if (args != -1 && signature.charAt(args + 1) == 'L') { - String type = signature.substring(args + 2, signature.length() - 1); - if (isInvalidPackage(type)) { - AbstractInsnNode first = nodes.size() > 0 ? nodes.get(0) : null; - record(context, method, first, type); - } - } - } - - for (int i = 0, n = nodes.size(); i < n; i++) { - AbstractInsnNode instruction = nodes.get(i); - int type = instruction.getType(); - if (type == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode node = (MethodInsnNode) instruction; - String owner = node.owner; - - // No need to check methods in this local class; we know they - // won't be an API match - if (node.getOpcode() == Opcodes.INVOKEVIRTUAL - && owner.equals(classNode.name)) { - owner = classNode.superName; - } - - while (owner != null) { - if (isInvalidPackage(owner)) { - record(context, method, instruction, owner); - } - - // For virtual dispatch, walk up the inheritance chain checking - // each inherited method - if (owner.startsWith("android/") //$NON-NLS-1$ - || owner.startsWith(JAVA_PKG_PREFIX) - || owner.startsWith(JAVAX_PKG_PREFIX)) { - owner = null; - } else if (node.getOpcode() == Opcodes.INVOKEVIRTUAL) { - owner = context.getDriver().getSuperClass(owner); - } else if (node.getOpcode() == Opcodes.INVOKESTATIC) { - // Inherit through static classes as well - owner = context.getDriver().getSuperClass(owner); - } else { - owner = null; - } - } - } else if (type == AbstractInsnNode.FIELD_INSN) { - FieldInsnNode node = (FieldInsnNode) instruction; - String owner = node.owner; - if (isInvalidPackage(owner)) { - record(context, method, instruction, owner); - } - } else if (type == AbstractInsnNode.LDC_INSN) { - LdcInsnNode node = (LdcInsnNode) instruction; - if (node.cst instanceof Type) { - Type t = (Type) node.cst; - String className = t.getInternalName(); - if (isInvalidPackage(className)) { - record(context, method, instruction, className); - } - } - } - } - } - } - - private boolean isInvalidPackage(String owner) { - if (owner.startsWith(JAVA_PKG_PREFIX) - || owner.startsWith(JAVAX_PKG_PREFIX)) { - return !mApiDatabase.isValidJavaPackage(owner); - } - - return false; - } - - private void record(ClassContext context, MethodNode method, - AbstractInsnNode instruction, String owner) { - if (owner.indexOf('$') != -1) { - // Don't report inner classes too; there will pretty much always be an outer class - // reference as well - return; - } - - if (mCandidates == null) { - mCandidates = Lists.newArrayList(); - } - mCandidates.add(new Candidate(owner, context.getClassNode().name, context.getJarFile())); - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mCandidates == null) { - return; - } - - for (Candidate candidate : mCandidates) { - String type = candidate.mClass; - if (mJavaxLibraryClasses.contains(type)) { - continue; - } - File jarFile = candidate.mJarFile; - String referencedIn = candidate.mReferencedIn; - - Location location = Location.create(jarFile); - Object pkg = getPackageName(type); - String message = String.format( - "Invalid package reference in library; not included in Android: %1$s. " + - "Referenced from %2$s.", pkg, ClassContext.getFqcn(referencedIn)); - context.report(ISSUE, location, message, null); - } - } - - private static Object getPackageName(String owner) { - String pkg = owner; - int index = pkg.lastIndexOf('/'); - if (index != -1) { - pkg = pkg.substring(0, index); - } - - return ClassContext.getFqcn(pkg); - } - - private static boolean shouldSkip(File file) { - // No need to do work on this library, which is included in pretty much all new ADT - // projects - if (file.getPath().endsWith("android-support-v4.jar")) { //$NON-NLS-1$ - return true; - } - - return false; - } - - private static class Candidate { - private final String mReferencedIn; - private final File mJarFile; - private final String mClass; - - public Candidate(String className, String referencedIn, File jarFile) { - mClass = className; - mReferencedIn = referencedIn; - mJarFile = jarFile; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/JavaPerformanceDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/JavaPerformanceDetector.java deleted file mode 100644 index b161699..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/JavaPerformanceDetector.java +++ /dev/null @@ -1,565 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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.Sets; -import com.google.common.collect.Sets.SetView; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.BinaryExpression; -import lombok.ast.BinaryOperator; -import lombok.ast.ConstructorInvocation; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.If; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.This; -import lombok.ast.Throw; -import lombok.ast.TypeReference; -import lombok.ast.TypeReferencePart; -import lombok.ast.UnaryExpression; -import lombok.ast.VariableDefinition; -import lombok.ast.VariableReference; - -/** - * Looks for performance issues in Java files, such as memory allocations during - * drawing operations and using HashMap instead of SparseArray. - */ -public class JavaPerformanceDetector extends Detector implements Detector.JavaScanner { - /** Allocating objects during a paint method */ - public static final Issue PAINT_ALLOC = Issue.create( - "DrawAllocation", //$NON-NLS-1$ - "Looks for memory allocations within drawing code", - - "You should avoid allocating objects during a drawing or layout operation. These " + - "are called frequently, so a smooth UI can be interrupted by garbage collection " + - "pauses caused by the object allocations.\n" + - "\n" + - "The way this is generally handled is to allocate the needed objects up front " + - "and to reuse them for each drawing operation.\n" + - "\n" + - "Some methods allocate memory on your behalf (such as `Bitmap.create`), and these " + - "should be handled in the same way.", - - Category.PERFORMANCE, - 9, - Severity.WARNING, - JavaPerformanceDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Using HashMaps where SparseArray would be better */ - public static final Issue USE_SPARSEARRAY = Issue.create( - "UseSparseArrays", //$NON-NLS-1$ - "Looks for opportunities to replace HashMaps with the more efficient SparseArray", - - "For maps where the keys are of type integer, it's typically more efficient to " + - "use the Android `SparseArray` API. This check identifies scenarios where you might " + - "want to consider using `SparseArray` instead of `HashMap` for better performance.\n" + - "\n" + - "This is *particularly* useful when the value types are primitives like ints, " + - "where you can use `SparseIntArray` and avoid auto-boxing the values from `int` to " + - "`Integer`.\n" + - "\n" + - "If you need to construct a `HashMap` because you need to call an API outside of " + - "your control which requires a `Map`, you can suppress this warning using for " + - "example the `@SuppressLint` annotation.", - - Category.PERFORMANCE, - 4, - Severity.WARNING, - JavaPerformanceDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Using {@code new Integer()} instead of the more efficient {@code Integer.valueOf} */ - public static final Issue USE_VALUEOF = Issue.create( - "UseValueOf", //$NON-NLS-1$ - "Looks for usages of \"new\" for wrapper classes which should use \"valueOf\" instead", - - "You should not call the constructor for wrapper classes directly, such as" + - "`new Integer(42)`. Instead, call the `valueOf` factory method, such as " + - "`Integer.valueOf(42)`. This will typically use less memory because common integers " + - "such as 0 and 1 will share a single instance.", - - Category.PERFORMANCE, - 4, - Severity.WARNING, - JavaPerformanceDetector.class, - Scope.JAVA_FILE_SCOPE); - - static final String ON_MEASURE = "onMeasure"; //$NON-NLS-1$ - static final String ON_DRAW = "onDraw"; //$NON-NLS-1$ - static final String ON_LAYOUT = "onLayout"; //$NON-NLS-1$ - private static final String INT = "int"; //$NON-NLS-1$ - private static final String INTEGER = "Integer"; //$NON-NLS-1$ - private static final String BOOL = "boolean"; //$NON-NLS-1$ - private static final String BOOLEAN = "Boolean"; //$NON-NLS-1$ - private static final String LONG = "Long"; //$NON-NLS-1$ - private static final String CHARACTER = "Character"; //$NON-NLS-1$ - private static final String DOUBLE = "Double"; //$NON-NLS-1$ - private static final String FLOAT = "Float"; //$NON-NLS-1$ - private static final String HASH_MAP = "HashMap"; //$NON-NLS-1$ - private static final String SPARSE_ARRAY = "SparseArray"; //$NON-NLS-1$ - private static final String CANVAS = "Canvas"; //$NON-NLS-1$ - private static final String LAYOUT = "layout"; //$NON-NLS-1$ - - /** Constructs a new {@link JavaPerformanceDetector} check */ - public JavaPerformanceDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements JavaScanner ---- - - @Override - public List<Class<? extends Node>> getApplicableNodeTypes() { - List<Class<? extends Node>> types = new ArrayList<Class<? extends Node>>(3); - types.add(ConstructorInvocation.class); - types.add(MethodDeclaration.class); - types.add(MethodInvocation.class); - return types; - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new PerformanceVisitor(context); - } - - private static class PerformanceVisitor extends ForwardingAstVisitor { - private final JavaContext mContext; - private final boolean mCheckMaps; - private final boolean mCheckAllocations; - private final boolean mCheckValueOf; - /** Whether allocations should be "flagged" in the current method */ - private boolean mFlagAllocations; - - public PerformanceVisitor(JavaContext context) { - mContext = context; - - mCheckAllocations = context.isEnabled(PAINT_ALLOC); - mCheckMaps = context.isEnabled(USE_SPARSEARRAY); - mCheckValueOf = context.isEnabled(USE_VALUEOF); - } - - @Override - public boolean visitMethodDeclaration(MethodDeclaration node) { - mFlagAllocations = isBlockedAllocationMethod(node); - - return super.visitMethodDeclaration(node); - } - - @Override - public boolean visitConstructorInvocation(ConstructorInvocation node) { - String typeName = null; - if (mCheckMaps) { - TypeReference reference = node.astTypeReference(); - typeName = reference.astParts().last().astIdentifier().astValue(); - // TODO: Should we handle factory method constructions of HashMaps as well, - // e.g. via Guava? This is a bit trickier since we need to infer the type - // arguments from the calling context. - if (typeName.equals(HASH_MAP)) { - checkHashMap(node, reference); - } else if (typeName.equals(SPARSE_ARRAY)) { - checkSparseArray(node, reference); - } - } - - if (mCheckValueOf) { - if (typeName == null) { - TypeReference reference = node.astTypeReference(); - typeName = reference.astParts().last().astIdentifier().astValue(); - } - if ((typeName.equals(INTEGER) - || typeName.equals(BOOLEAN) - || typeName.equals(FLOAT) - || typeName.equals(CHARACTER) - || typeName.equals(LONG) - || typeName.equals(DOUBLE)) - && node.astTypeReference().astParts().size() == 1 - && node.astArguments().size() == 1) { - String argument = node.astArguments().first().toString(); - mContext.report(USE_VALUEOF, node, mContext.getLocation(node), - String.format("Use %1$s.valueOf(%2$s) instead", typeName, argument), - null); - } - } - - if (mFlagAllocations && !(node.getParent() instanceof Throw) && mCheckAllocations) { - // Make sure we're still inside the method declaration that marked - // mInDraw as true, in case we've left it and we're in a static - // block or something: - Node method = node; - while (method != null) { - if (method instanceof MethodDeclaration) { - break; - } - method = method.getParent(); - } - if (method != null && isBlockedAllocationMethod(((MethodDeclaration) method)) - && !isLazilyInitialized(node)) { - reportAllocation(node); - } - } - - return super.visitConstructorInvocation(node); - } - - private void reportAllocation(Node node) { - mContext.report(PAINT_ALLOC, node, mContext.getLocation(node), - "Avoid object allocations during draw/layout operations (preallocate and " + - "reuse instead)", null); - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (mFlagAllocations && node.astOperand() != null) { - // Look for forbidden methods - String methodName = node.astName().astValue(); - if (methodName.equals("createBitmap") //$NON-NLS-1$ - || methodName.equals("createScaledBitmap")) { //$NON-NLS-1$ - String operand = node.astOperand().toString(); - if (operand.equals("Bitmap") //$NON-NLS-1$ - || operand.equals("android.graphics.Bitmap")) { //$NON-NLS-1$ - if (!isLazilyInitialized(node)) { - reportAllocation(node); - } - } - } else if (methodName.startsWith("decode")) { //$NON-NLS-1$ - // decodeFile, decodeByteArray, ... - String operand = node.astOperand().toString(); - if (operand.equals("BitmapFactory") //$NON-NLS-1$ - || operand.equals("android.graphics.BitmapFactory")) { //$NON-NLS-1$ - if (!isLazilyInitialized(node)) { - reportAllocation(node); - } - } - } else if (methodName.equals("getClipBounds")) { //$NON-NLS-1$ - if (node.astArguments().isEmpty()) { - mContext.report(PAINT_ALLOC, node, mContext.getLocation(node), - "Avoid object allocations during draw operations: Use " + - "Canvas.getClipBounds(Rect) instead of Canvas.getClipBounds() " + - "which allocates a temporary Rect", null); - } - } - } - - return super.visitMethodInvocation(node); - } - - /** - * Check whether the given invocation is done as a lazy initialization, - * e.g. {@code if (foo == null) foo = new Foo();}. - * <p> - * This tries to also handle the scenario where the check is on some - * <b>other</b> variable - e.g. - * <pre> - * if (foo == null) { - * foo == init1(); - * bar = new Bar(); - * } - * </pre> - * or - * <pre> - * if (!initialized) { - * initialized = true; - * bar = new Bar(); - * } - * </pre> - */ - private static boolean isLazilyInitialized(Node node) { - Node curr = node.getParent(); - while (curr != null) { - if (curr instanceof MethodDeclaration) { - return false; - } else if (curr instanceof If) { - If ifNode = (If) curr; - // See if the if block represents a lazy initialization: - // compute all variable names seen in the condition - // (e.g. for "if (foo == null || bar != foo)" the result is "foo,bar"), - // and then compute all variables assigned to in the if body, - // and if there is an overlap, we'll consider the whole if block - // guarded (so lazily initialized and an allocation we won't complain - // about.) - List<String> assignments = new ArrayList<String>(); - AssignmentTracker visitor = new AssignmentTracker(assignments); - ifNode.astStatement().accept(visitor); - if (!assignments.isEmpty()) { - List<String> references = new ArrayList<String>(); - addReferencedVariables(references, ifNode.astCondition()); - if (!references.isEmpty()) { - SetView<String> intersection = Sets.intersection( - new HashSet<String>(assignments), - new HashSet<String>(references)); - return !intersection.isEmpty(); - } - } - return false; - - } - curr = curr.getParent(); - } - - return false; - } - - /** Adds any variables referenced in the given expression into the given list */ - private static void addReferencedVariables(Collection<String> variables, - Expression expression) { - if (expression instanceof BinaryExpression) { - BinaryExpression binary = (BinaryExpression) expression; - addReferencedVariables(variables, binary.astLeft()); - addReferencedVariables(variables, binary.astRight()); - } else if (expression instanceof UnaryExpression) { - UnaryExpression unary = (UnaryExpression) expression; - addReferencedVariables(variables, unary.astOperand()); - } else if (expression instanceof VariableReference) { - VariableReference reference = (VariableReference) expression; - variables.add(reference.astIdentifier().astValue()); - } else if (expression instanceof Select) { - Select select = (Select) expression; - if (select.astOperand() instanceof This) { - variables.add(select.astIdentifier().astValue()); - } - } - } - - /** - * Returns whether the given method declaration represents a method - * where allocating objects is not allowed for performance reasons - */ - private static boolean isBlockedAllocationMethod(MethodDeclaration node) { - return isOnDrawMethod(node) || isOnMeasureMethod(node) || isOnLayoutMethod(node) - || isLayoutMethod(node); - } - - /** - * Returns true if this method looks like it's overriding android.view.View's - * {@code protected void onDraw(Canvas canvas)} - */ - private static boolean isOnDrawMethod(MethodDeclaration node) { - if (ON_DRAW.equals(node.astMethodName().astValue())) { - StrictListAccessor<VariableDefinition, MethodDeclaration> parameters = - node.astParameters(); - if (parameters != null && parameters.size() == 1) { - VariableDefinition arg0 = parameters.first(); - TypeReferencePart type = arg0.astTypeReference().astParts().last(); - String typeName = type.getTypeName(); - if (typeName.equals(CANVAS)) { - return true; - } - } - } - - return false; - } - - /** - * Returns true if this method looks like it's overriding - * android.view.View's - * {@code protected void onLayout(boolean changed, int left, int top, - * int right, int bottom)} - */ - private static boolean isOnLayoutMethod(MethodDeclaration node) { - if (ON_LAYOUT.equals(node.astMethodName().astValue())) { - StrictListAccessor<VariableDefinition, MethodDeclaration> parameters = - node.astParameters(); - if (parameters != null && parameters.size() == 5) { - Iterator<VariableDefinition> iterator = parameters.iterator(); - if (!iterator.hasNext()) { - return false; - } - - // Ensure that the argument list matches boolean, int, int, int, int - TypeReferencePart type = iterator.next().astTypeReference().astParts().last(); - if (!type.getTypeName().equals(BOOL) || !iterator.hasNext()) { - return false; - } - for (int i = 0; i < 4; i++) { - type = iterator.next().astTypeReference().astParts().last(); - if (!type.getTypeName().equals(INT)) { - return false; - } - if (!iterator.hasNext()) { - return i == 3; - } - } - } - } - - return false; - } - - /** - * Returns true if this method looks like it's overriding android.view.View's - * {@code protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)} - */ - private static boolean isOnMeasureMethod(MethodDeclaration node) { - if (ON_MEASURE.equals(node.astMethodName().astValue())) { - StrictListAccessor<VariableDefinition, MethodDeclaration> parameters = - node.astParameters(); - if (parameters != null && parameters.size() == 2) { - VariableDefinition arg0 = parameters.first(); - VariableDefinition arg1 = parameters.last(); - TypeReferencePart type1 = arg0.astTypeReference().astParts().last(); - TypeReferencePart type2 = arg1.astTypeReference().astParts().last(); - return INT.equals(type1.getTypeName()) && INT.equals(type2.getTypeName()); - } - } - - return false; - } - - /** - * Returns true if this method looks like it's overriding android.view.View's - * {@code public void layout(int l, int t, int r, int b)} - */ - private static boolean isLayoutMethod(MethodDeclaration node) { - if (LAYOUT.equals(node.astMethodName().astValue())) { - StrictListAccessor<VariableDefinition, MethodDeclaration> parameters = - node.astParameters(); - if (parameters != null && parameters.size() == 4) { - Iterator<VariableDefinition> iterator = parameters.iterator(); - for (int i = 0; i < 4; i++) { - if (!iterator.hasNext()) { - return false; - } - VariableDefinition next = iterator.next(); - TypeReferencePart type = next.astTypeReference().astParts().last(); - if (!INT.equals(type.getTypeName())) { - return false; - } - } - return true; - } - } - - return false; - } - - - /** - * Checks whether the given constructor call and type reference refers - * to a HashMap constructor call that is eligible for replacement by a - * SparseArray call instead - */ - private void checkHashMap(ConstructorInvocation node, TypeReference reference) { - // reference.hasTypeArguments returns false where it should not - StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments(); - if (types != null && types.size() == 2) { - TypeReference first = types.first(); - if (first.getTypeName().equals(INTEGER)) { - String valueType = types.last().getTypeName(); - if (valueType.equals(INTEGER)) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseIntArray(...) instead for better performance", - null); - } else if (valueType.equals(BOOLEAN)) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseBooleanArray(...) instead for better performance", - null); - } else if (valueType.equals(LONG) && mContext.getProject().getMinSdk() >= 17) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseLongArray(...) instead for better performance", - null); - } else { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - String.format( - "Use new SparseArray<%1$s>(...) instead for better performance", - valueType), - null); - } - } - } - } - - private void checkSparseArray(ConstructorInvocation node, TypeReference reference) { - // reference.hasTypeArguments returns false where it should not - StrictListAccessor<TypeReference, TypeReference> types = reference.getTypeArguments(); - if (types != null && types.size() == 1) { - TypeReference first = types.first(); - String valueType = first.getTypeName(); - if (valueType.equals(INTEGER)) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseIntArray(...) instead for better performance", - null); - } else if (valueType.equals(BOOLEAN)) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseBooleanArray(...) instead for better performance", - null); - } else if (valueType.equals(LONG) && mContext.getProject().getMinSdk() >= 17) { - mContext.report(USE_SPARSEARRAY, node, mContext.getLocation(node), - "Use new SparseLongArray(...) instead for better performance", - null); - } - } - } - } - - /** Visitor which records variable names assigned into */ - private static class AssignmentTracker extends ForwardingAstVisitor { - private final Collection<String> mVariables; - - public AssignmentTracker(Collection<String> variables) { - mVariables = variables; - } - - @Override - public boolean visitBinaryExpression(BinaryExpression node) { - BinaryOperator operator = node.astOperator(); - if (operator == BinaryOperator.ASSIGN || operator == BinaryOperator.OR_ASSIGN) { - Expression left = node.astLeft(); - String variable; - if (left instanceof Select && ((Select) left).astOperand() instanceof This) { - variable = ((Select) left).astIdentifier().astValue(); - } else { - variable = left.toString(); - } - mVariables.add(variable); - } - - return super.visitBinaryExpression(node); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LabelForDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LabelForDetector.java deleted file mode 100644 index 283e244..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LabelForDetector.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_URI; -import static com.android.SdkConstants.ATTR_HINT; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_LABEL_FOR; -import static com.android.SdkConstants.AUTO_COMPLETE_TEXT_VIEW; -import static com.android.SdkConstants.EDIT_TEXT; -import static com.android.SdkConstants.ID_PREFIX; -import static com.android.SdkConstants.MULTI_AUTO_COMPLETE_TEXT_VIEW; -import static com.android.SdkConstants.NEW_ID_PREFIX; -import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; -import com.google.common.collect.Sets; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Set; - -/** - * Detector which finds unlabeled text fields - */ -public class LabelForDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "LabelFor", //$NON-NLS-1$ - "Ensures that text fields are marked with a labelFor attribute", - "Text fields should be labelled with a `labelFor` attribute, " + - "provided your `minSdkVersion` is at least 17.\n" + - "\n" + - "If your view is labeled but by a label in a different layout which " + - "includes this one, just suppress this warning from lint.", - Category.A11Y, - 2, - Severity.WARNING, - LabelForDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - private Set<String> mLabels; - private List<Element> mTextFields; - - /** Constructs a new {@link LabelForDetector} */ - public LabelForDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - @Nullable - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_LABEL_FOR); - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - EDIT_TEXT, - AUTO_COMPLETE_TEXT_VIEW, - MULTI_AUTO_COMPLETE_TEXT_VIEW - ); - } - - @Override - public void afterCheckFile(@NonNull Context context) { - if (mTextFields != null) { - if (mLabels == null) { - mLabels = Collections.emptySet(); - } - - for (Element element : mTextFields) { - if (element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) { - continue; - } - String id = element.getAttributeNS(ANDROID_URI, ATTR_ID); - boolean missing = true; - if (mLabels.contains(id)) { - missing = false; - } else if (id.startsWith(NEW_ID_PREFIX)) { - missing = !mLabels.contains(ID_PREFIX + stripIdPrefix(id)); - } else if (id.startsWith(ID_PREFIX)) { - missing = !mLabels.contains(NEW_ID_PREFIX + stripIdPrefix(id)); - } - - if (missing) { - XmlContext xmlContext = (XmlContext) context; - Location location = xmlContext.getLocation(element); - String message; - if (id == null || id.isEmpty()) { - message = "No label views point to this text field with a " + - "labelFor attribute"; - } else { - message = String.format("No label views point to this text field with " + - "an android:labelFor=\"@+id/%1$s\" attribute", id); - } - xmlContext.report(ISSUE, element, location, message, null); - } - - } - } - - mLabels = null; - mTextFields = null; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (mLabels == null) { - mLabels = Sets.newHashSet(); - } - mLabels.add(attribute.getValue()); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - // NOTE: This should NOT be checking *minSdkVersion*, but *targetSdkVersion* - // or even buildTarget instead. However, there's a risk that this will flag - // way too much and make the rule annoying until API 17 support becomes - // more widespread, so for now limit the check to those projects *really* - // working with 17. When API 17 reaches a certain amount of adoption, change - // this to flag all apps supporting 17, including those supporting earlier - // versions as well. - if (context.getMainProject().getMinSdk() < 17) { - return; - } - - if (mTextFields == null) { - mTextFields = new ArrayList<Element>(); - } - mTextFields.add(element); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LocaleDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LocaleDetector.java deleted file mode 100644 index 3bde211..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/LocaleDetector.java +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright (C) 2012 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.CONSTRUCTOR_NAME; -import static com.android.SdkConstants.FORMAT_METHOD; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -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 org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.Frame; -import org.objectweb.asm.tree.analysis.SourceInterpreter; -import org.objectweb.asm.tree.analysis.SourceValue; - -import java.util.Arrays; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; - -/** - * Checks for errors related to locale handling - */ -public class LocaleDetector extends Detector implements ClassScanner { - /** Calling risky convenience methods */ - public static final Issue STRING_LOCALE = Issue.create( - "DefaultLocale", //$NON-NLS-1$ - "Finds calls to locale-ambiguous String manipulation methods", - - "Calling `String#toLowerCase()` or `#toUpperCase()` *without specifying an " + - "explicit locale* is a common source of bugs. The reason for that is that those " + - "methods will use the current locale on the user's device, and even though the " + - "code appears to work correctly when you are developing the app, it will fail " + - "in some locales. For example, in the Turkish locale, the uppercase replacement " + - "for `i` is *not* `I`.\n" + - "\n" + - "If you want the methods to just perform ASCII replacement, for example to convert " + - "an enum name, call `String#toUpperCase(Locale.US)` instead. If you really want to " + - "use the current locale, call `String#toUpperCase(Locale.getDefault())` instead.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - LocaleDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.CLASS_FILE)).setMoreInfo( - "http://developer.android.com/reference/java/util/Locale.html#default_locale"); //$NON-NLS-1$ - - /** Constructing SimpleDateFormat without an explicit locale */ - public static final Issue DATE_FORMAT = Issue.create( - "SimpleDateFormat", //$NON-NLS-1$ - "Using SimpleDateFormat directly without an explicit locale", - - "Almost all callers should use `getDateInstance()`, `getDateTimeInstance()`, or " + - "`getTimeInstance()` to get a ready-made instance of SimpleDateFormat suitable " + - "for the user's locale. The main reason you'd create an instance this class " + - "directly is because you need to format/parse a specific machine-readable format, " + - "in which case you almost certainly want to explicitly ask for US to ensure that " + - "you get ASCII digits (rather than, say, Arabic digits).\n" + - "\n" + - "Therefore, you should either use the form of the SimpleDateFormat constructor " + - "where you pass in an explicit locale, such as Locale.US, or use one of the " + - "get instance methods, or suppress this error if really know what you are doing.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - LocaleDetector.class, - Scope.CLASS_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/reference/java/text/SimpleDateFormat.html"); //$NON-NLS-1$ - - static final String DATE_FORMAT_OWNER = "java/text/SimpleDateFormat"; //$NON-NLS-1$ - private static final String STRING_OWNER = "java/lang/String"; //$NON-NLS-1$ - - /** Constructs a new {@link LocaleDetector} */ - public LocaleDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Arrays.asList( - "toLowerCase", //$NON-NLS-1$ - "toUpperCase", //$NON-NLS-1$ - FORMAT_METHOD - ); - } - - @Override - @Nullable - public List<String> getApplicableCallOwners() { - return Collections.singletonList(DATE_FORMAT_OWNER); - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - String owner = call.owner; - String desc = call.desc; - String name = call.name; - if (owner.equals(DATE_FORMAT_OWNER)) { - if (!name.equals(CONSTRUCTOR_NAME)) { - return; - } - if (desc.equals("(Ljava/lang/String;Ljava/text/DateFormatSymbols;)V") //$NON-NLS-1$ - || desc.equals("()V") //$NON-NLS-1$ - || desc.equals("(Ljava/lang/String;)V")) { //$NON-NLS-1$ - Location location = context.getLocation(call); - String message = - "To get local formatting use getDateInstance(), getDateTimeInstance(), " + - "or getTimeInstance(), or use new SimpleDateFormat(String template, " + - "Locale locale) with for example Locale.US for ASCII dates."; - context.report(DATE_FORMAT, method, call, location, message, null); - } - return; - } else if (!owner.equals(STRING_OWNER)) { - return; - } - - if (name.equals(FORMAT_METHOD)) { - // Only check the non-locale version of String.format - if (!desc.equals("(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;")) { //$NON-NLS-1$ - return; - } - // Find the formatting string - Analyzer analyzer = new Analyzer(new SourceInterpreter() { - @Override - public SourceValue newOperation(AbstractInsnNode insn) { - if (insn.getOpcode() == Opcodes.LDC) { - Object cst = ((LdcInsnNode) insn).cst; - if (cst instanceof String) { - return new StringValue(1, (String) cst); - } - } - return super.newOperation(insn); - } - }); - try { - Frame[] frames = analyzer.analyze(classNode.name, method); - InsnList instructions = method.instructions; - Frame frame = frames[instructions.indexOf(call)]; - if (frame.getStackSize() == 0) { - return; - } - SourceValue stackValue = (SourceValue) frame.getStack(0); - if (stackValue instanceof StringValue) { - String format = ((StringValue) stackValue).getString(); - if (format != null && StringFormatDetector.isLocaleSpecific(format)) { - Location location = context.getLocation(call); - String message = - "Implicitly using the default locale is a common source of bugs: " + - "Use String.format(Locale, ...) instead"; - context.report(STRING_LOCALE, method, call, location, message, null); - } - } - } catch (AnalyzerException e) { - context.log(e, null); - } - } else { - if (desc.equals("()Ljava/lang/String;")) { //$NON-NLS-1$ - Location location = context.getLocation(call); - String message = String.format( - "Implicitly using the default locale is a common source of bugs: " + - "Use %1$s(Locale) instead", name); - context.report(STRING_LOCALE, method, call, location, message, null); - } - } - } - - private static class StringValue extends SourceValue { - private final String mString; - - StringValue(int size, String string) { - super(size); - mString = string; - } - - String getString() { - return mString; - } - - @Override - public int getSize() { - return 1; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestOrderDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestOrderDetector.java deleted file mode 100644 index 83fac97..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestOrderDetector.java +++ /dev/null @@ -1,557 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_MIN_SDK_VERSION; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PACKAGE; -import static com.android.SdkConstants.ATTR_TARGET_SDK_VERSION; -import static com.android.SdkConstants.PREFIX_RESOURCE_REF; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_APPLICATION; -import static com.android.SdkConstants.TAG_PERMISSION; -import static com.android.SdkConstants.TAG_PROVIDER; -import static com.android.SdkConstants.TAG_RECEIVER; -import static com.android.SdkConstants.TAG_SERVICE; -import static com.android.SdkConstants.TAG_USES_LIBRARY; -import static com.android.SdkConstants.TAG_USES_PERMISSION; -import static com.android.SdkConstants.TAG_USES_SDK; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -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.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.android.tools.lint.detector.api.XmlContext; -import com.google.common.collect.Maps; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -/** - * Checks for issues in AndroidManifest files such as declaring elements in the - * wrong order. - */ -public class ManifestOrderDetector extends Detector implements Detector.XmlScanner { - - /** Wrong order of elements in the manifest */ - public static final Issue ORDER = Issue.create( - "ManifestOrder", //$NON-NLS-1$ - "Checks for manifest problems like <uses-sdk> after the <application> tag", - "The <application> tag should appear after the elements which declare " + - "which version you need, which features you need, which libraries you " + - "need, and so on. In the past there have been subtle bugs (such as " + - "themes not getting applied correctly) when the `<application>` tag appears " + - "before some of these other elements, so it's best to order your " + - "manifest in the logical dependency order.", - Category.CORRECTNESS, - 5, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE); - - /** Missing a {@code <uses-sdk>} element */ - public static final Issue USES_SDK = Issue.create( - "UsesMinSdkAttributes", //$NON-NLS-1$ - "Checks that the minimum SDK and target SDK attributes are defined", - - "The manifest should contain a `<uses-sdk>` element which defines the " + - "minimum API Level required for the application to run, " + - "as well as the target version (the highest API level you have tested " + - "the version for.)", - - Category.CORRECTNESS, - 9, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$ - - /** Using a targetSdkVersion that isn't recent */ - public static final Issue TARGET_NEWER = Issue.create( - "OldTargetApi", //$NON-NLS-1$ - "Checks that the manifest specifies a targetSdkVersion that is recent", - - "When your application runs on a version of Android that is more recent than your " + - "`targetSdkVersion` specifies that it has been tested with, various compatibility " + - "modes kick in. This ensures that your application continues to work, but it may " + - "look out of place. For example, if the `targetSdkVersion` is less than 14, your " + - "app may get an option button in the UI.\n" + - "\n" + - "To fix this issue, set the `targetSdkVersion` to the highest available value. Then " + - "test your app to make sure everything works correctly. You may want to consult " + - "the compatibility notes to see what changes apply to each version you are adding " + - "support for: " + - "http://developer.android.com/reference/android/os/Build.VERSION_CODES.html", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/reference/android/os/Build.VERSION_CODES.html"); //$NON-NLS-1$ - - /** Using multiple {@code <uses-sdk>} elements */ - public static final Issue MULTIPLE_USES_SDK = Issue.create( - "MultipleUsesSdk", //$NON-NLS-1$ - "Checks that the <uses-sdk> element appears at most once", - - "The `<uses-sdk>` element should appear just once; the tools will *not* merge the " + - "contents of all the elements so if you split up the attributes across multiple " + - "elements, only one of them will take effect. To fix this, just merge all the " + - "attributes from the various elements into a single <uses-sdk> element.", - - Category.CORRECTNESS, - 6, - Severity.FATAL, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/manifest/uses-sdk-element.html"); //$NON-NLS-1$ - - /** Missing a {@code <uses-sdk>} element */ - public static final Issue WRONG_PARENT = Issue.create( - "WrongManifestParent", //$NON-NLS-1$ - "Checks that various manifest elements are declared in the right place", - - "The `<uses-library>` element should be defined as a direct child of the " + - "`<application>` tag, not the `<manifest>` tag or an `<activity>` tag. Similarly, " + - "a `<uses-sdk>` tag much be declared at the root level, and so on. This check " + - "looks for incorrect declaration locations in the manifest, and complains " + - "if an element is found in the wrong place.", - - Category.CORRECTNESS, - 6, - Severity.FATAL, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$ - - /** Missing a {@code <uses-sdk>} element */ - public static final Issue DUPLICATE_ACTIVITY = Issue.create( - "DuplicateActivity", //$NON-NLS-1$ - "Checks that an activity is registered only once in the manifest", - - "An activity should only be registered once in the manifest. If it is " + - "accidentally registered more than once, then subtle errors can occur, " + - "since attribute declarations from the two elements are not merged, so " + - "you may accidentally remove previous declarations.", - - Category.CORRECTNESS, - 5, - Severity.ERROR, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE); - - /** Not explicitly defining allowBackup */ - public static final Issue ALLOW_BACKUP = Issue.create( - "AllowBackup", //$NON-NLS-1$ - "Ensure that allowBackup is explicitly set in the application's manifest", - - "The allowBackup attribute determines if an application's data can be backed up " + - "and restored. It is documented at " + - "http://developer.android.com/reference/android/R.attr.html#allowBackup\n" + - "\n" + - "By default, this flag is set to `true`. When this flag is set to `true`, " + - "application data can be backed up and restored by the user using `adb backup` " + - "and `adb restore`.\n" + - "\n" + - "This may have security consequences for an application. `adb backup` allows " + - "users who have enabled USB debugging to copy application data off of the " + - "device. Once backed up, all application data can be read by the user. " + - "`adb restore` allows creation of application data from a source specified " + - "by the user. Following a restore, applications should not assume that the " + - "data, file permissions, and directory permissions were created by the " + - "application itself.\n" + - "\n" + - "Setting `allowBackup=\"false\"` opts an application out of both backup and " + - "restore.\n" + - "\n" + - "To fix this warning, decide whether your application should support backup, " + - "and explicitly set `android:allowBackup=(true|false)\"`", - - Category.SECURITY, - 3, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/reference/android/R.attr.html#allowBackup"); - - /** Conflicting permission names */ - public static final Issue UNIQUE_PERMISSION = Issue.create( - "UniquePermission", //$NON-NLS-1$ - "Checks that permission names are unique", - - "The unqualified names or your permissions must be unique. The reason for this " + - "is that at build time, the `aapt` tool will generate a class named `Manifest` " + - "which contains a field for each of your permissions. These fields are named " + - "using your permission unqualified names (i.e. the name portion after the last " + - "dot).\n" + - "\n" + - "If more than one permission maps to the same field name, that field will " + - "arbitrarily name just one of them.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE); - - /** Using a resource for attributes that do not allow it */ - public static final Issue SET_VERSION = Issue.create( - "MissingVersion", //$NON-NLS-1$ - "Checks that the application name and version are set", - - "You should define the version information for your application.\n" + - "`android:versionCode`: An integer value that represents the version of the " + - "application code, relative to other versions.\n" + - "\n" + - "`android:versionName`: A string value that represents the release version of " + - "the application code, as it should be shown to users.", - - Category.CORRECTNESS, - 2, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE).setMoreInfo( - "http://developer.android.com/tools/publishing/versioning.html#appversioning"); - - /** Using a resource for attributes that do not allow it */ - public static final Issue ILLEGAL_REFERENCE = Issue.create( - "IllegalResourceRef", //$NON-NLS-1$ - "Checks for resource references where only literals are allowed", - - "For the `versionCode` attribute, you have to specify an actual integer " + - "literal; you cannot use an indirection with a `@dimen/name` resource. " + - "Similarly, the `versionName` attribute should be an actual string, not " + - "a string resource url.", - - Category.CORRECTNESS, - 8, - Severity.WARNING, - ManifestOrderDetector.class, - Scope.MANIFEST_SCOPE); - - /** Constructs a new {@link ManifestOrderDetector} check */ - public ManifestOrderDetector() { - } - - private boolean mSeenApplication; - - /** Number of times we've seen the <uses-sdk> element */ - private int mSeenUsesSdk; - - /** Activities we've encountered */ - private final Set<String> mActivities = new HashSet<String>(); - - /** Permission basenames */ - private Map<String, String> mPermissionNames; - - /** Package declared in the manifest */ - private String mPackage; - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - mSeenApplication = false; - mSeenUsesSdk = 0; - } - - @Override - public void afterCheckFile(@NonNull Context context) { - XmlContext xmlContext = (XmlContext) context; - Element element = xmlContext.document.getDocumentElement(); - if (element != null) { - checkDocumentElement(xmlContext, element); - } - - if (mSeenUsesSdk == 0 && context.isEnabled(USES_SDK)) { - context.report(USES_SDK, Location.create(context.file), - "Manifest should specify a minimum API level with " + - "<uses-sdk android:minSdkVersion=\"?\" />; if it really supports " + - "all versions of Android set it to 1.", null); - } - } - - private void checkDocumentElement(XmlContext context, Element element) { - Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, "versionCode");//$NON-NLS-1$ - if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF) - && context.isEnabled(ILLEGAL_REFERENCE)) { - context.report(ILLEGAL_REFERENCE, element, context.getLocation(element), - "The android:versionCode cannot be a resource url, it must be " - + "a literal integer", null); - } else if (codeNode == null && context.isEnabled(SET_VERSION)) { - context.report(SET_VERSION, element, context.getLocation(element), - "Should set android:versionCode to specify the application version", null); - } - Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, "versionName");//$NON-NLS-1$ - if (nameNode != null && nameNode.getValue().startsWith(PREFIX_RESOURCE_REF) - && context.isEnabled(ILLEGAL_REFERENCE)) { - context.report(ILLEGAL_REFERENCE, element, context.getLocation(element), - "The android:versionName cannot be a resource url, it must be " - + "a literal string", null); - } else if (nameNode == null && context.isEnabled(SET_VERSION)) { - context.report(SET_VERSION, element, context.getLocation(element), - "Should set android:versionName to specify the application version", null); - } - } - - // ---- Implements Detector.XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_APPLICATION, - TAG_USES_PERMISSION, - TAG_PERMISSION, - "permission-tree", //$NON-NLS-1$ - "permission-group", //$NON-NLS-1$ - TAG_USES_SDK, - "uses-configuration", //$NON-NLS-1$ - "uses-feature", //$NON-NLS-1$ - "supports-screens", //$NON-NLS-1$ - "compatible-screens", //$NON-NLS-1$ - "supports-gl-texture", //$NON-NLS-1$ - TAG_USES_LIBRARY, - TAG_ACTIVITY, - TAG_SERVICE, - TAG_PROVIDER, - TAG_RECEIVER - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - Node parentNode = element.getParentNode(); - - if (tag.equals(TAG_USES_LIBRARY) || tag.equals(TAG_ACTIVITY) || tag.equals(TAG_SERVICE) - || tag.equals(TAG_PROVIDER) || tag.equals(TAG_RECEIVER)) { - if (!TAG_APPLICATION.equals(parentNode.getNodeName()) - && context.isEnabled(WRONG_PARENT)) { - context.report(WRONG_PARENT, element, context.getLocation(element), - String.format( - "The <%1$s> element must be a direct child of the <application> element", - tag), null); - } - - if (tag.equals(TAG_ACTIVITY)) { - Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); - if (nameNode != null) { - String name = nameNode.getValue(); - if (!name.isEmpty()) { - if (name.charAt(0) == '.') { - name = getPackage(element) + name; - } else if (name.indexOf('.') == -1) { - name = getPackage(element) + '.' + name; - } - if (mActivities.contains(name)) { - String message = String.format( - "Duplicate registration for activity %1$s", name); - context.report(DUPLICATE_ACTIVITY, element, - context.getLocation(nameNode), message, null); - } else { - mActivities.add(name); - } - } - } - } - - return; - } - - if (parentNode != element.getOwnerDocument().getDocumentElement() - && context.isEnabled(WRONG_PARENT)) { - context.report(WRONG_PARENT, element, context.getLocation(element), - String.format( - "The <%1$s> element must be a direct child of the " + - "<manifest> root element", tag), null); - } - - if (tag.equals(TAG_USES_SDK)) { - mSeenUsesSdk++; - - if (mSeenUsesSdk == 2) { // Only warn when we encounter the first one - Location location = context.getLocation(element); - - // Link up *all* encountered locations in the document - NodeList elements = element.getOwnerDocument().getElementsByTagName(TAG_USES_SDK); - Location secondary = null; - for (int i = elements.getLength() - 1; i >= 0; i--) { - Element e = (Element) elements.item(i); - if (e != element) { - Location l = context.getLocation(e); - l.setSecondary(secondary); - l.setMessage("Also appears here"); - secondary = l; - } - } - location.setSecondary(secondary); - - if (context.isEnabled(MULTIPLE_USES_SDK)) { - context.report(MULTIPLE_USES_SDK, element, location, - "There should only be a single <uses-sdk> element in the manifest:" + - " merge these together", null); - } - return; - } - - if (!element.hasAttributeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION)) { - if (context.isEnabled(USES_SDK)) { - context.report(USES_SDK, element, context.getLocation(element), - "<uses-sdk> tag should specify a minimum API level with " + - "android:minSdkVersion=\"?\"", null); - } - } else { - Attr codeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_MIN_SDK_VERSION); - if (codeNode != null && codeNode.getValue().startsWith(PREFIX_RESOURCE_REF) - && context.isEnabled(ILLEGAL_REFERENCE)) { - context.report(ILLEGAL_REFERENCE, element, context.getLocation(element), - "The android:minSdkVersion cannot be a resource url, it must be " - + "a literal integer (or string if a preview codename)", null); - } - } - - if (!element.hasAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION)) { - // Warn if not setting target SDK -- but only if the min SDK is somewhat - // old so there's some compatibility stuff kicking in (such as the menu - // button etc) - if (context.isEnabled(USES_SDK)) { - context.report(USES_SDK, element, context.getLocation(element), - "<uses-sdk> tag should specify a target API level (the " + - "highest verified version; when running on later versions, " + - "compatibility behaviors may be enabled) with " + - "android:targetSdkVersion=\"?\"", null); - } - } else if (context.isEnabled(TARGET_NEWER)){ - String target = element.getAttributeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); - try { - int api = Integer.parseInt(target); - if (api < context.getClient().getHighestKnownApiLevel()) { - context.report(TARGET_NEWER, element, context.getLocation(element), - "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.", - null); - } - } catch (NumberFormatException nufe) { - // Ignore: AAPT will enforce this. - } - } - - Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_TARGET_SDK_VERSION); - if (nameNode != null && nameNode.getValue().startsWith(PREFIX_RESOURCE_REF) - && context.isEnabled(ILLEGAL_REFERENCE)) { - context.report(ILLEGAL_REFERENCE, element, context.getLocation(element), - "The android:targetSdkVersion cannot be a resource url, it must be " - + "a literal integer (or string if a preview codename)", null); - } - } - if (tag.equals(TAG_PERMISSION)) { - Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); - if (nameNode != null) { - String name = nameNode.getValue(); - String base = name.substring(name.lastIndexOf('.') + 1); - if (mPermissionNames == null) { - mPermissionNames = Maps.newHashMap(); - } else if (mPermissionNames.containsKey(base)) { - String prevName = mPermissionNames.get(base); - Location location = context.getLocation(nameNode); - NodeList siblings = element.getParentNode().getChildNodes(); - for (int i = 0, n = siblings.getLength(); i < n; i++) { - Node node = siblings.item(i); - if (node == element) { - break; - } else if (node.getNodeType() == Node.ELEMENT_NODE) { - Element sibling = (Element) node; - String suffix = '.' + base; - if (sibling.getTagName().equals(TAG_PERMISSION)) { - String b = element.getAttributeNS(ANDROID_URI, ATTR_NAME); - if (b.endsWith(suffix)) { - Location prevLocation = context.getLocation(node); - prevLocation.setMessage("Previous permission here"); - location.setSecondary(prevLocation); - break; - } - - } - } - } - - String message = String.format("Permission name %1$s is not unique " + - "(appears in both %2$s and %3$s)", base, prevName, name); - context.report(UNIQUE_PERMISSION, element, location, message, null); - } - - mPermissionNames.put(base, name); - } - } - - if (tag.equals(TAG_APPLICATION)) { - mSeenApplication = true; - if (!element.hasAttributeNS(ANDROID_URI, SdkConstants.ATTR_ALLOW_BACKUP) - && context.isEnabled(ALLOW_BACKUP) - && context.getMainProject().getMinSdk() >= 4) { - context.report(ALLOW_BACKUP, element, context.getLocation(element), - "Should explicitly set android:allowBackup to true or " + - "false (it's true by default, and that can have some security " + - "implications for the application's data)", null); - } - } else if (mSeenApplication) { - if (context.isEnabled(ORDER)) { - context.report(ORDER, element, context.getLocation(element), - String.format("<%1$s> tag appears after <application> tag", tag), null); - } - - // Don't complain for *every* element following the <application> tag - mSeenApplication = false; - } - } - - private String getPackage(Element element) { - if (mPackage == null) { - mPackage = element.getOwnerDocument().getDocumentElement().getAttribute(ATTR_PACKAGE); - } - - return mPackage; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestTypoDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestTypoDetector.java deleted file mode 100644 index cc85be6..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ManifestTypoDetector.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright (C) 2013 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.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.TAG_USES_FEATURE; -import static com.android.SdkConstants.TAG_USES_LIBRARY; -import static com.android.SdkConstants.TAG_USES_PERMISSION; -import static com.android.SdkConstants.TAG_USES_SDK; - -import com.android.SdkConstants; -import com.android.annotations.NonNull; -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.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.android.tools.lint.detector.api.XmlContext; -import com.google.common.collect.Maps; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Checks for typos in AndroidManifest files. - */ -public class ManifestTypoDetector extends Detector implements Detector.XmlScanner { - - private static final String REPORT_FORMAT - = "<%1$s> looks like a typo; did you mean <%2$s> ?"; - - /* The match pattern for <uses-sdk> */ - private static final Pattern PATTERN_USES_SDK - = Pattern.compile("^use.*sdk"); //$NON-NLS-1$ - - /* The match pattern for <uses-permission> */ - private static final Pattern PATTERN_USES_PERMISSION - = Pattern.compile("^use.*permission"); //$NON-NLS-1$ - - /* The match pattern for <uses-feature> */ - private static final Pattern PATTERN_USES_FEATURE - = Pattern.compile("^use.*feature"); //$NON-NLS-1$ - - /* The match pattern for <uses-library> */ - private static final Pattern PATTERN_USES_LIBRARY - = Pattern.compile("^use.*library"); //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ManifestTypo", //$NON-NLS-1$ - "Checks for manifest typos", - - "This check looks through the manifest, and if it finds any tags " + - "that look like likely misspellings, they are flagged.", - Category.CORRECTNESS, - 5, - Severity.WARNING, - ManifestTypoDetector.class, - Scope.MANIFEST_SCOPE); - - /** Constructs a new {@link ManifestTypoDetector} check */ - public ManifestTypoDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - @Override - public Collection<String> getApplicableElements() { - return XmlScanner.ALL; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - - if (!tag.startsWith("use")) { //$NON-NLS-1$ - return; - } - - if (PATTERN_USES_SDK.matcher(tag).find() && !TAG_USES_SDK.equals(tag)) { - context.report(ISSUE, context.getLocation(element), - String.format(REPORT_FORMAT, tag, TAG_USES_SDK), null); - } - - if (PATTERN_USES_PERMISSION.matcher(tag).find() && !TAG_USES_PERMISSION.equals(tag)) { - context.report(ISSUE, context.getLocation(element), - String.format(REPORT_FORMAT, tag, TAG_USES_PERMISSION), null); - } - - if (PATTERN_USES_FEATURE.matcher(tag).find() && !TAG_USES_FEATURE.equals(tag)) { - context.report(ISSUE, context.getLocation(element), - String.format(REPORT_FORMAT, tag, TAG_USES_FEATURE), null); - } - - if (PATTERN_USES_LIBRARY.matcher(tag).find() && !TAG_USES_LIBRARY.equals(tag)) { - context.report(ISSUE, context.getLocation(element), - String.format(REPORT_FORMAT, tag, TAG_USES_LIBRARY), null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MathDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MathDetector.java deleted file mode 100644 index 8709852..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MathDetector.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -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 org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.Arrays; -import java.util.List; - -/** - * Looks for usages of {@link java.lang.Math} methods which can be replaced with - * {@code android.util.FloatMath} methods to avoid casting. - */ -public class MathDetector extends Detector implements Detector.ClassScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "FloatMath", //$NON-NLS-1$ - "Suggests replacing android.util.FloatMath calls with java.lang.Math", - - "In older versions of Android, using android.util.FloatMath was recommended " + - "for performance reasons when operating on floats. However, on modern hardware " + - "doubles are just as fast as float (though they take more memory), and in " + - "recent versions of Android, FloatMath is actually slower than using java.lang.Math " + - "due to the way the JIT optimizes java.lang.Math. Therefore, you should use " + - "Math instead of FloatMath if you are only targeting Froyo and above.", - - Category.PERFORMANCE, - 3, - Severity.WARNING, - MathDetector.class, - Scope.CLASS_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/design/performance.html#avoidfloat"); //$NON-NLS-1$ - - /** Constructs a new {@link MathDetector} check */ - public MathDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Arrays.asList( - "sin", //$NON-NLS-1$ - "cos", //$NON-NLS-1$ - "ceil", //$NON-NLS-1$ - "sqrt", //$NON-NLS-1$ - "floor" //$NON-NLS-1$ - ); - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - String owner = call.owner; - - if (owner.equals("android/util/FloatMath") //$NON-NLS-1$ - && context.getProject().getMinSdk() >= 8) { - String message = String.format( - "Use java.lang.Math#%1$s instead of android.util.FloatMath#%1$s() " + - "since it is faster as of API 8", call.name); - context.report(ISSUE, method, call, context.getLocation(call), message, null /*data*/); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MergeRootFrameLayoutDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MergeRootFrameLayoutDetector.java deleted file mode 100644 index 1b76c03..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MergeRootFrameLayoutDetector.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BACKGROUND; -import static com.android.SdkConstants.ATTR_FOREGROUND; -import static com.android.SdkConstants.ATTR_LAYOUT; -import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.FRAME_LAYOUT; -import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.R_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.VIEW_INCLUDE; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -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.XmlContext; -import com.android.utils.Pair; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import lombok.ast.AstVisitor; -import lombok.ast.Expression; -import lombok.ast.MethodInvocation; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; - -/** - * Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag. - */ -public class MergeRootFrameLayoutDetector extends LayoutDetector implements Detector.JavaScanner { - /** - * Set of layouts that we want to enable the warning for. We only warn for - * {@code <FrameLayout>}'s that are the root of a layout included from - * another layout, or directly referenced via a {@code setContentView} call. - */ - private Set<String> mWhitelistedLayouts; - - /** - * Set of pending [layout, location] pairs where the given layout is a - * FrameLayout that perhaps should be replaced by a {@code <merge>} tag (if - * the layout is included or set as the content view. This must be processed - * after the whole project has been scanned since the set of includes etc - * can be encountered after the included layout. - */ - private List<Pair<String, Location.Handle>> mPending; - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "MergeRootFrame", //$NON-NLS-1$ - "Checks whether a root <FrameLayout> can be replaced with a <merge> tag", - "If a `<FrameLayout>` is the root of a layout and does not provide background " + - "or padding etc, it can often be replaced with a `<merge>` tag which is slightly " + - "more efficient. Note that this depends on context, so make sure you understand " + - "how the `<merge>` tag works before proceeding.", - Category.PERFORMANCE, - 4, - Severity.WARNING, - MergeRootFrameLayoutDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE)).setMoreInfo( - "http://android-developers.blogspot.com/2009/03/android-layout-tricks-3-optimize-by.html"); //$NON-NLS-1$ - - /** Constructs a new {@link MergeRootFrameLayoutDetector} */ - public MergeRootFrameLayoutDetector() { - } - - @Override - @NonNull - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA); - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mPending != null && mWhitelistedLayouts != null) { - // Process all the root FrameLayouts that are eligible, and generate - // suggestions for <merge> replacements for any layouts that are included - // from other layouts - for (Pair<String, Handle> pair : mPending) { - String layout = pair.getFirst(); - if (mWhitelistedLayouts.contains(layout)) { - Handle handle = pair.getSecond(); - - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ISSUE, (Node) clientData)) { - return; - } - } - - Location location = handle.resolve(); - context.report(ISSUE, location, - "This <FrameLayout> can be replaced with a <merge> tag", null); - } - } - } - } - - // Implements XmlScanner - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList(VIEW_INCLUDE, FRAME_LAYOUT); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - if (tag.equals(VIEW_INCLUDE)) { - String layout = element.getAttribute(ATTR_LAYOUT); // NOTE: Not in android: namespace - if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts - layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); - whiteListLayout(layout); - } - } else { - assert tag.equals(FRAME_LAYOUT); - if (LintUtils.isRootElement(element) && - ((isWidthFillParent(element) && isHeightFillParent(element)) || - !element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_GRAVITY)) - && !element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND) - && !element.hasAttributeNS(ANDROID_URI, ATTR_FOREGROUND) - && !hasPadding(element)) { - String layout = LintUtils.getLayoutName(context.file); - Handle handle = context.parser.createLocationHandle(context, element); - handle.setClientData(element); - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - if (mPending == null) { - mPending = new ArrayList<Pair<String,Handle>>(); - } - mPending.add(Pair.of(layout, handle)); - } - } - } - - private void whiteListLayout(String layout) { - if (mWhitelistedLayouts == null) { - mWhitelistedLayouts = new HashSet<String>(); - } - mWhitelistedLayouts.add(layout); - } - - // Implements JavaScanner - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("setContentView"); //$NON-NLS-1$ - } - - @Override - public void visitMethod( - @NonNull JavaContext context, - @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - StrictListAccessor<Expression, MethodInvocation> argumentList = node.astArguments(); - if (argumentList != null && argumentList.size() == 1) { - Expression argument = argumentList.first(); - if (argument instanceof Select) { - String expression = argument.toString(); - if (expression.startsWith(R_LAYOUT_RESOURCE_PREFIX)) { - whiteListLayout(expression.substring(R_LAYOUT_RESOURCE_PREFIX.length())); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingClassDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingClassDetector.java deleted file mode 100644 index 8002c40..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingClassDetector.java +++ /dev/null @@ -1,406 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_PKG_PREFIX; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_CLASS; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PACKAGE; -import static com.android.SdkConstants.CONSTRUCTOR_NAME; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_APPLICATION; -import static com.android.SdkConstants.TAG_PROVIDER; -import static com.android.SdkConstants.TAG_RECEIVER; -import static com.android.SdkConstants.TAG_SERVICE; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.VIEW_FRAGMENT; -import static com.android.SdkConstants.VIEW_TAG; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -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.XmlContext; -import com.android.utils.SdkUtils; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Checks to ensure that classes referenced in the manifest actually exist and are included - * - */ -public class MissingClassDetector extends LayoutDetector implements ClassScanner { - /** Manifest-referenced classes missing from the project or libraries */ - public static final Issue MISSING = Issue.create( - "MissingRegistered", //$NON-NLS-1$ - "Ensures that classes referenced in the manifest are present in the project or libraries", - - "If a class is referenced in the manifest, it must also exist in the project (or in one " + - "of the libraries included by the project. This check helps uncover typos in " + - "registration names, or attempts to rename or move classes without updating the " + - "manifest file properly.", - - Category.CORRECTNESS, - 8, - Severity.ERROR, - MissingClassDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.CLASS_FILE, Scope.JAVA_LIBRARIES, Scope.RESOURCE_FILE)) - .setMoreInfo("http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$ - - /** Are activity, service, receiver etc subclasses instantiatable? */ - public static final Issue INSTANTIATABLE = Issue.create( - "Instantiatable", //$NON-NLS-1$ - "Ensures that classes registered in the manifest file are instantiatable", - - "Activities, services, broadcast receivers etc. registered in the manifest file " + - "must be \"instantiatable\" by the system, which means that the class must be " + - "public, it must have an empty public constructor, and if it's an inner class, " + - "it must be a static inner class.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - MissingClassDetector.class, - Scope.CLASS_FILE_SCOPE); - - /** Is the right character used for inner class separators? */ - public static final Issue INNERCLASS = Issue.create( - "InnerclassSeparator", //$NON-NLS-1$ - "Ensures that inner classes are referenced using '$' instead of '.' in class names", - - "When you reference an inner class in a manifest file, you must use '$' instead of '.' " + - "as the separator character, i.e. Outer$Inner instead of Outer.Inner.\n" + - "\n" + - "(If you get this warning for a class which is not actually an inner class, it's " + - "because you are using uppercase characters in your package name, which is not " + - "conventional.)", - - Category.CORRECTNESS, - 3, - Severity.WARNING, - MissingClassDetector.class, - Scope.MANIFEST_SCOPE); - - private Map<String, Location.Handle> mReferencedClasses; - private Set<String> mCustomViews; - private boolean mHaveClasses; - - /** Constructs a new {@link MissingClassDetector} */ - public MissingClassDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return ALL; - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES || folderType == ResourceFolderType.LAYOUT; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String pkg = null; - Node classNameNode; - String className; - String tag = element.getTagName(); - ResourceFolderType folderType = context.getResourceFolderType(); - if (folderType == ResourceFolderType.VALUES) { - if (!tag.equals(TAG_STRING)) { - return; - } - Attr attr = element.getAttributeNode(ATTR_NAME); - if (attr == null) { - return; - } - className = attr.getValue(); - classNameNode = attr; - } else if (folderType == ResourceFolderType.LAYOUT) { - if (tag.indexOf('.') > 0) { - className = tag; - classNameNode = element; - } else if (tag.equals(VIEW_FRAGMENT) || tag.equals(VIEW_TAG)) { - Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); - if (attr == null) { - attr = element.getAttributeNode(ATTR_CLASS); - } - if (attr == null) { - return; - } - className = attr.getValue(); - classNameNode = attr; - } else { - return; - } - } else { - // Manifest file - if (TAG_APPLICATION.equals(tag) - || TAG_ACTIVITY.equals(tag) - || TAG_SERVICE.equals(tag) - || TAG_RECEIVER.equals(tag) - || TAG_PROVIDER.equals(tag)) { - Element root = element.getOwnerDocument().getDocumentElement(); - pkg = root.getAttribute(ATTR_PACKAGE); - Attr attr = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); - if (attr == null) { - return; - } - className = attr.getValue(); - classNameNode = attr; - } else { - return; - } - } - if (className.isEmpty()) { - return; - } - - String fqcn; - int dotIndex = className.indexOf('.'); - if (dotIndex <= 0) { - if (pkg == null) { - return; // value file - } - if (dotIndex == 0) { - fqcn = pkg + className; - } else { - // According to the <activity> manifest element documentation, this is not - // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html ) - // but it appears in manifest files and appears to be supported by the runtime - // so handle this in code as well: - fqcn = pkg + '.' + className; - } - } else { // else: the class name is already a fully qualified class name - fqcn = className; - // Only look for fully qualified tracker names in analytics files - if (folderType == ResourceFolderType.VALUES - && !SdkUtils.endsWith(context.file.getPath(), "analytics.xml")) { //$NON-NLS-1$ - return; - } - } - - String signature = ClassContext.getInternalName(fqcn); - if (signature.isEmpty() || signature.startsWith(ANDROID_PKG_PREFIX)) { - return; - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - Handle handle = null; - if (!context.getDriver().isSuppressed(MISSING, element)) { - if (mReferencedClasses == null) { - mReferencedClasses = Maps.newHashMapWithExpectedSize(16); - mCustomViews = Sets.newHashSetWithExpectedSize(8); - } - - handle = context.parser.createLocationHandle(context, element); - mReferencedClasses.put(signature, handle); - if (folderType == ResourceFolderType.LAYOUT && !tag.equals(VIEW_FRAGMENT)) { - mCustomViews.add(ClassContext.getInternalName(className)); - } - } - - if (signature.indexOf('$') != -1 && pkg != null) { - if (className.indexOf('$') == -1 && className.indexOf('.', 1) > 0) { - boolean haveUpperCase = false; - for (int i = 0, n = pkg.length(); i < n; i++) { - if (Character.isUpperCase(pkg.charAt(i))) { - haveUpperCase = true; - break; - } - } - if (!haveUpperCase) { - String message = "Use '$' instead of '.' for inner classes " + - "(or use only lowercase letters in package names)"; - Location location = context.getLocation(classNameNode); - context.report(INNERCLASS, element, location, message, null); - } - } - - // The internal name contains a $ which means it's an inner class. - // The conversion from fqcn to internal name is a bit ambiguous: - // "a.b.C.D" usually means "inner class D in class C in package a.b". - // However, it can (see issue 31592) also mean class D in package "a.b.C". - // To make sure we don't falsely complain that foo/Bar$Baz doesn't exist, - // in case the user has actually created a package named foo/Bar and a proper - // class named Baz, we register *both* into the reference map. - // When generating errors we'll look for these an rip them back out if - // it looks like one of the two variations have been seen. - if (handle != null) { - signature = signature.replace('$', '/'); - mReferencedClasses.put(signature, handle); - } - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (!context.getProject().isLibrary() && mHaveClasses - && mReferencedClasses != null && !mReferencedClasses.isEmpty() - && context.getDriver().getScope().contains(Scope.CLASS_FILE)) { - List<String> classes = new ArrayList<String>(mReferencedClasses.keySet()); - Collections.sort(classes); - for (String owner : classes) { - Location.Handle handle = mReferencedClasses.get(owner); - String fqcn = ClassContext.getFqcn(owner); - - String signature = ClassContext.getInternalName(fqcn); - if (!signature.equals(owner)) { - if (!mReferencedClasses.containsKey(signature)) { - continue; - } - } else { - signature = signature.replace('$', '/'); - if (!mReferencedClasses.containsKey(signature)) { - continue; - } - } - mReferencedClasses.remove(owner); - - // Ignore usages of platform libraries - if (owner.startsWith("android/")) { //$NON-NLS-1$ - continue; - } - - String message = String.format( - "Class referenced in the manifest, %1$s, was not found in the " + - "project or the libraries", fqcn); - Location location = handle.resolve(); - File parentFile = location.getFile().getParentFile(); - if (parentFile != null) { - String parent = parentFile.getName(); - ResourceFolderType type = ResourceFolderType.getFolderType(parent); - if (type == ResourceFolderType.LAYOUT) { - message = String.format( - "Class referenced in the layout file, %1$s, was not found in " - + "the project or the libraries", fqcn); - } else if (type == ResourceFolderType.VALUES) { - message = String.format( - "Class referenced in the analytics file, %1$s, was not " - + "found in the project or the libraries", fqcn); - } - } - - context.report(MISSING, location, message, null); - } - } - } - - // ---- Implements ClassScanner ---- - - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if (!mHaveClasses && !context.isFromClassLibrary() - && context.getProject() == context.getMainProject()) { - mHaveClasses = true; - } - String curr = classNode.name; - if (mReferencedClasses != null && mReferencedClasses.containsKey(curr)) { - boolean isCustomView = mCustomViews.contains(curr); - mReferencedClasses.remove(curr); - - // Ensure that the class is public, non static and has a null constructor! - - if ((classNode.access & Opcodes.ACC_PUBLIC) == 0) { - context.report(INSTANTIATABLE, context.getLocation(classNode), String.format( - "This class should be public (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - return; - } - - if (classNode.name.indexOf('$') != -1 && !LintUtils.isStaticInnerClass(classNode)) { - context.report(INSTANTIATABLE, context.getLocation(classNode), String.format( - "This inner class should be static (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - return; - } - - boolean hasDefaultConstructor = false; - @SuppressWarnings("rawtypes") // ASM API - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - if (method.name.equals(CONSTRUCTOR_NAME)) { - if (method.desc.equals("()V")) { //$NON-NLS-1$ - // The constructor must be public - if ((method.access & Opcodes.ACC_PUBLIC) != 0) { - hasDefaultConstructor = true; - } else { - context.report(INSTANTIATABLE, context.getLocation(method, classNode), - "The default constructor must be public", - null); - // Also mark that we have a constructor so we don't complain again - // below since we've already emitted a more specific error related - // to the default constructor - hasDefaultConstructor = true; - } - } - } - } - - if (!hasDefaultConstructor && !isCustomView && !context.isFromClassLibrary() - && context.getProject().getReportIssues()) { - context.report(INSTANTIATABLE, context.getLocation(classNode), String.format( - "This class should provide a default constructor (a public " + - "constructor with no arguments) (%1$s)", - ClassContext.createSignature(classNode.name, null, null)), - null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingIdDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingIdDetector.java deleted file mode 100644 index 1b79600..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/MissingIdDetector.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_URI; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_TAG; -import static com.android.SdkConstants.VIEW_FRAGMENT; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; - -/** - * Check which looks for missing id's in views where they are probably needed - */ -public class MissingIdDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "MissingId", //$NON-NLS-1$ - "Ensures that XML tags like <fragment> specify an id or tag attribute", - - "If you do not specify an android:id or an android:tag attribute on a " + - "<fragment> element, then if the activity is restarted (for example for " + - "an orientation rotation) you may lose state. From the fragment " + - "documentation:\n" + - "\n" + - "\"Each fragment requires a unique identifier that the system can use " + - "to restore the fragment if the activity is restarted (and which you can " + - "use to capture the fragment to perform transactions, such as remove it). " + - "* Supply the android:id attribute with a unique ID.\n" + - "* Supply the android:tag attribute with a unique string.\n" + - "If you provide neither of the previous two, the system uses the ID of the " + - "container view.", - - Category.CORRECTNESS, - 5, - Severity.WARNING, - MissingIdDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setMoreInfo("http://developer.android.com/guide/components/fragments.html"); //$NON-NLS-1$ - - /** Constructs a new {@link MissingIdDetector} */ - public MissingIdDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(VIEW_FRAGMENT); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (!element.hasAttributeNS(ANDROID_URI, ATTR_ID) && - !element.hasAttributeNS(ANDROID_URI, ATTR_TAG)) { - context.report(ISSUE, element, context.getLocation(element), - "This <fragment> tag should specify an id or a tag to preserve state " + - "across activity restarts", null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NamespaceDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NamespaceDetector.java deleted file mode 100644 index 0b6ab02..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NamespaceDetector.java +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_URI; -import static com.android.SdkConstants.AUTO_URI; -import static com.android.SdkConstants.URI_PREFIX; -import static com.android.SdkConstants.XMLNS_PREFIX; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.HashMap; -import java.util.Map; - -/** - * Checks for various issues related to XML namespaces - */ -public class NamespaceDetector extends LayoutDetector { - /** Typos in the namespace */ - public static final Issue TYPO = Issue.create( - "NamespaceTypo", //$NON-NLS-1$ - "Looks for misspellings in namespace declarations", - - "Accidental misspellings in namespace declarations can lead to some very " + - "obscure error messages. This check looks for potential misspellings to " + - "help track these down.", - Category.CORRECTNESS, - 8, - Severity.WARNING, - NamespaceDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Unused namespace declarations */ - public static final Issue UNUSED = Issue.create( - "UnusedNamespace", //$NON-NLS-1$ - "Finds unused namespaces in XML documents", - - "Unused namespace declarations take up space and require processing that is not " + - "necessary", - - Category.PERFORMANCE, - 1, - Severity.WARNING, - NamespaceDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Using custom namespace attributes in a library project */ - public static final Issue CUSTOMVIEW = Issue.create( - "LibraryCustomView", //$NON-NLS-1$ - "Flags custom attributes in libraries, which must use the res-auto-namespace instead", - - "When using a custom view with custom attributes in a library project, the layout " + - "must use the special namespace " + AUTO_URI + " instead of a URI which includes " + - "the library project's own package. This will be used to automatically adjust the " + - "namespace of the attributes when the library resources are merged into the " + - "application project.", - Category.CORRECTNESS, - 6, - Severity.ERROR, - NamespaceDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Prefix relevant for custom namespaces */ - private static final String XMLNS_ANDROID = "xmlns:android"; //$NON-NLS-1$ - private static final String XMLNS_A = "xmlns:a"; //$NON-NLS-1$ - - private Map<String, Attr> mUnusedNamespaces; - private boolean mCheckUnused; - - /** Constructs a new {@link NamespaceDetector} */ - public NamespaceDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - boolean haveCustomNamespace = false; - Element root = document.getDocumentElement(); - NamedNodeMap attributes = root.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Node item = attributes.item(i); - if (item.getNodeName().startsWith(XMLNS_PREFIX)) { - String value = item.getNodeValue(); - - if (!value.equals(ANDROID_URI)) { - Attr attribute = (Attr) item; - - if (value.startsWith(URI_PREFIX)) { - haveCustomNamespace = true; - if (mUnusedNamespaces == null) { - mUnusedNamespaces = new HashMap<String, Attr>(); - } - mUnusedNamespaces.put(item.getNodeName().substring(XMLNS_PREFIX.length()), - attribute); - } else if (!value.startsWith("http://")) { //$NON-NLS-1$ - context.report(TYPO, attribute, context.getLocation(attribute), - "Suspicious namespace: should start with http://", null); - - continue; - } - - String name = attribute.getName(); - if (!name.equals(XMLNS_ANDROID) && !name.equals(XMLNS_A)) { - // See if it looks like a typo - int resIndex = value.indexOf("/res/"); //$NON-NLS-1$ - if (resIndex != -1 && value.length() + 5 > URI_PREFIX.length()) { - String urlPrefix = value.substring(0, resIndex + 5); - if (!urlPrefix.equals(URI_PREFIX) && - LintUtils.editDistance(URI_PREFIX, urlPrefix) <= 3) { - String correctUri = URI_PREFIX + value.substring(resIndex + 5); - context.report(TYPO, attribute, context.getLocation(attribute), - String.format( - "Possible typo in URL: was \"%1$s\", should " + - "probably be \"%2$s\"", - value, correctUri), - null); - } - } - continue; - } - - if (!context.isEnabled(TYPO)) { - continue; - } - - if (name.equals(XMLNS_A)) { - // For the "android" prefix we always assume that the namespace prefix - // should be our expected prefix, but for the "a" prefix we make sure - // that it's at least "close"; if you're bound it to something completely - // different, don't complain. - if (LintUtils.editDistance(ANDROID_URI, value) > 4) { - continue; - } - } - - if (value.equalsIgnoreCase(ANDROID_URI)) { - context.report(TYPO, attribute, context.getLocation(attribute), - String.format( - "URI is case sensitive: was \"%1$s\", expected \"%2$s\"", - value, ANDROID_URI), null); - } else { - context.report(TYPO, attribute, context.getLocation(attribute), - String.format( - "Unexpected namespace URI bound to the \"android\" " + - "prefix, was %1$s, expected %2$s", value, ANDROID_URI), - null); - } - } - } - } - - if (haveCustomNamespace) { - boolean checkCustomAttrs = context.isEnabled(CUSTOMVIEW) && context.getProject().isLibrary(); - mCheckUnused = context.isEnabled(UNUSED); - - if (checkCustomAttrs) { - checkCustomNamespace(context, root); - } - checkElement(context, root); - - if (mCheckUnused && !mUnusedNamespaces.isEmpty()) { - for (Map.Entry<String, Attr> entry : mUnusedNamespaces.entrySet()) { - String prefix = entry.getKey(); - Attr attribute = entry.getValue(); - context.report(UNUSED, attribute, context.getLocation(attribute), - String.format("Unused namespace %1$s", prefix), null); - } - } - } - } - - private static void checkCustomNamespace(XmlContext context, Element element) { - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attribute = (Attr) attributes.item(i); - if (attribute.getName().startsWith(XMLNS_PREFIX)) { - String uri = attribute.getValue(); - if (uri != null && !uri.isEmpty() && uri.startsWith(URI_PREFIX) - && !uri.equals(ANDROID_URI)) { - context.report(CUSTOMVIEW, attribute, context.getLocation(attribute), - "When using a custom namespace attribute in a library project, " + - "use the namespace \"" + AUTO_URI + "\" instead.", null); - } - } - } - } - - private void checkElement(XmlContext context, Node node) { - if (node.getNodeType() == Node.ELEMENT_NODE) { - if (mCheckUnused) { - NamedNodeMap attributes = node.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attribute = (Attr) attributes.item(i); - String prefix = attribute.getPrefix(); - if (prefix != null) { - mUnusedNamespaces.remove(prefix); - } - } - } - - NodeList childNodes = node.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - checkElement(context, childNodes.item(i)); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NestedScrollingWidgetDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NestedScrollingWidgetDetector.java deleted file mode 100644 index 4650a8f..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NestedScrollingWidgetDetector.java +++ /dev/null @@ -1,155 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.GALLERY; -import static com.android.SdkConstants.GRID_VIEW; -import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; -import static com.android.SdkConstants.LIST_VIEW; -import static com.android.SdkConstants.SCROLL_VIEW; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.Arrays; -import java.util.Collection; - -/** - * Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag. - */ -public class NestedScrollingWidgetDetector extends LayoutDetector { - private int mVisitingHorizontalScroll; - private int mVisitingVerticalScroll; - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "NestedScrolling", //$NON-NLS-1$ - "Checks whether a scrolling widget has any nested scrolling widgets within", - // TODO: Better description! - "A scrolling widget such as a `ScrollView` should not contain any nested " + - "scrolling widgets since this has various usability issues", - Category.CORRECTNESS, - 7, - Severity.WARNING, - NestedScrollingWidgetDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link NestedScrollingWidgetDetector} */ - public NestedScrollingWidgetDetector() { - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - mVisitingHorizontalScroll = 0; - mVisitingVerticalScroll = 0; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - SCROLL_VIEW, - LIST_VIEW, - GRID_VIEW, - // Horizontal - GALLERY, - HORIZONTAL_SCROLL_VIEW - ); - } - - private Element findOuterScrollingWidget(Node node, boolean vertical) { - Collection<String> applicableElements = getApplicableElements(); - while (node != null) { - if (node instanceof Element) { - Element element = (Element) node; - String tagName = element.getTagName(); - if (applicableElements.contains(tagName) - && vertical == isVerticalScroll(element)) { - return element; - } - } - - node = node.getParentNode(); - } - - return null; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - boolean vertical = isVerticalScroll(element); - if (vertical) { - mVisitingVerticalScroll++; - } else { - mVisitingHorizontalScroll++; - } - - if (mVisitingHorizontalScroll > 1 || mVisitingVerticalScroll > 1) { - Element parent = findOuterScrollingWidget(element.getParentNode(), vertical); - if (parent != null) { - String format; - if (mVisitingVerticalScroll > 1) { - format = "The vertically scrolling %1$s should not contain another " + - "vertically scrolling widget (%2$s)"; - } else { - format = "The horizontally scrolling %1$s should not contain another " + - "horizontally scrolling widget (%2$s)"; - } - String msg = String.format(format, parent.getTagName(), element.getTagName()); - context.report(ISSUE, element, context.getLocation(element), msg, null); - } - } - } - - @Override - public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) { - if (isVerticalScroll(element)) { - mVisitingVerticalScroll--; - assert mVisitingVerticalScroll >= 0; - } else { - mVisitingHorizontalScroll--; - assert mVisitingHorizontalScroll >= 0; - } - } - - private static boolean isVerticalScroll(Element element) { - String view = element.getTagName(); - if (view.equals(GALLERY) || view.equals(HORIZONTAL_SCROLL_VIEW)) { - return false; - } else { - // This method should only be called with one of the 5 widget types - // listed in getApplicableElements - assert view.equals(SCROLL_VIEW) || view.equals(LIST_VIEW) || view.equals(GRID_VIEW); - return true; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NonInternationalizedSmsDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NonInternationalizedSmsDetector.java deleted file mode 100644 index e970572..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/NonInternationalizedSmsDetector.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.Expression; -import lombok.ast.MethodInvocation; -import lombok.ast.StrictListAccessor; -import lombok.ast.StringLiteral; - -/** Detector looking for text messages sent to an unlocalized phone number. */ -public class NonInternationalizedSmsDetector extends Detector implements Detector.JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "UnlocalizedSms", //$NON-NLS-1$ - "Looks for code sending text messages to unlocalized phone numbers", - - "SMS destination numbers must start with a country code or the application code " + - "must ensure that the SMS is only sent when the user is in the same country as " + - "the receiver.", - - Category.CORRECTNESS, - 5, - Severity.WARNING, - NonInternationalizedSmsDetector.class, - Scope.JAVA_FILE_SCOPE); - - - /** Constructs a new {@link NonInternationalizedSmsDetector} check */ - public NonInternationalizedSmsDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - - // ---- Implements JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - List<String> methodNames = new ArrayList<String>(2); - methodNames.add("sendTextMessage"); //$NON-NLS-1$ - methodNames.add("sendMultipartTextMessage"); //$NON-NLS-1$ - return methodNames; - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - assert node.astName().astValue().equals("sendTextMessage") || //$NON-NLS-1$ - node.astName().astValue().equals("sendMultipartTextMessage"); //$NON-NLS-1$ - if (node.astOperand() == null) { - // "sendTextMessage"/"sendMultipartTextMessage" in the code with no operand - return; - } - - StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); - if (args.size() == 5) { - Expression destinationAddress = args.first(); - if (destinationAddress instanceof StringLiteral) { - String number = ((StringLiteral) destinationAddress).astValue(); - - if (!number.startsWith("+")) { //$NON-NLS-1$ - context.report(ISSUE, node, context.getLocation(destinationAddress), - "To make sure the SMS can be sent by all users, please start the SMS number " + - "with a + and a country code or restrict the code invocation to people in the country " + - "you are targeting.", - null); - } - } - } - } -}
\ No newline at end of file diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java deleted file mode 100644 index 559a7ad..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ObsoleteLayoutParamsDetector.java +++ /dev/null @@ -1,424 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ABSOLUTE_LAYOUT; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_LAYOUT; -import static com.android.SdkConstants.ATTR_LAYOUT_ABOVE; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BASELINE; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_BOTTOM; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_LEFT; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_BOTTOM; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_LEFT; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_RIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_PARENT_TOP; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_RIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_TOP; -import static com.android.SdkConstants.ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING; -import static com.android.SdkConstants.ATTR_LAYOUT_BELOW; -import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_HORIZONTAL; -import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_IN_PARENT; -import static com.android.SdkConstants.ATTR_LAYOUT_CENTER_VERTICAL; -import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN; -import static com.android.SdkConstants.ATTR_LAYOUT_COLUMN_SPAN; -import static com.android.SdkConstants.ATTR_LAYOUT_GRAVITY; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN; -import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_BOTTOM; -import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_LEFT; -import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_RIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_MARGIN_TOP; -import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.ATTR_LAYOUT_ROW; -import static com.android.SdkConstants.ATTR_LAYOUT_ROW_SPAN; -import static com.android.SdkConstants.ATTR_LAYOUT_SPAN; -import static com.android.SdkConstants.ATTR_LAYOUT_TO_LEFT_OF; -import static com.android.SdkConstants.ATTR_LAYOUT_TO_RIGHT_OF; -import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.ATTR_LAYOUT_X; -import static com.android.SdkConstants.ATTR_LAYOUT_Y; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.GRID_LAYOUT; -import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.RELATIVE_LAYOUT; -import static com.android.SdkConstants.TABLE_ROW; -import static com.android.SdkConstants.VIEW_INCLUDE; -import static com.android.SdkConstants.VIEW_MERGE; -import static com.android.SdkConstants.VIEW_TAG; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.IDomParser; -import com.android.tools.lint.client.api.SdkInfo; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -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.XmlContext; -import com.android.utils.Pair; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Looks for layout params on views that are "obsolete" - may have made sense - * when the view was added but there is a different layout parent now which does - * not use the given layout params. - */ -public class ObsoleteLayoutParamsDetector extends LayoutDetector { - /** Usage of deprecated views or attributes */ - public static final Issue ISSUE = Issue.create( - "ObsoleteLayoutParam", //$NON-NLS-1$ - "Looks for layout params that are not valid for the given parent layout", - "The given layout_param is not defined for the given layout, meaning it has no " + - "effect. This usually happens when you change the parent layout or move view " + - "code around without updating the layout params. This will cause useless " + - "attribute processing at runtime, and is misleading for others reading the " + - "layout so the parameter should be removed.", - Category.PERFORMANCE, - 6, - Severity.WARNING, - ObsoleteLayoutParamsDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** - * Set of layout parameter names that are considered valid no matter what so - * no other checking is necessary - such as layout_width and layout_height. - */ - private static final Set<String> VALID = new HashSet<String>(10); - - /** - * Mapping from a layout parameter name (local name only) to the defining - * ViewGroup. Note that it's possible for the same name to be defined by - * multiple ViewGroups - but it turns out this is extremely rare (the only - * examples are layout_column defined by both TableRow and GridLayout, and - * layout_gravity defined by many layouts) so rather than handle this with - * every single layout attribute pointing to a list, this is just special - * cased instead. - */ - private static final Map<String, String> PARAM_TO_VIEW = new HashMap<String, String>(28); - - static { - // Available (mostly) everywhere: No check - VALID.add(ATTR_LAYOUT_WIDTH); - VALID.add(ATTR_LAYOUT_HEIGHT); - - // The layout_gravity isn't "global" but it's defined on many of the most - // common layouts (FrameLayout, LinearLayout and GridLayout) so we don't - // currently check for it. In order to do this we'd need to make the map point - // to lists rather than individual layouts or we'd need a bunch of special cases - // like the one done for layout_column below. - VALID.add(ATTR_LAYOUT_GRAVITY); - - // From ViewGroup.MarginLayoutParams - VALID.add(ATTR_LAYOUT_MARGIN_LEFT); - VALID.add(ATTR_LAYOUT_MARGIN_RIGHT); - VALID.add(ATTR_LAYOUT_MARGIN_TOP); - VALID.add(ATTR_LAYOUT_MARGIN_BOTTOM); - VALID.add(ATTR_LAYOUT_MARGIN); - - // Absolute Layout - PARAM_TO_VIEW.put(ATTR_LAYOUT_X, ABSOLUTE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_Y, ABSOLUTE_LAYOUT); - - // Linear Layout - PARAM_TO_VIEW.put(ATTR_LAYOUT_WEIGHT, LINEAR_LAYOUT); - - // Grid Layout - PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, GRID_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN_SPAN, GRID_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW, GRID_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ROW_SPAN, GRID_LAYOUT); - - // Table Layout - // ATTR_LAYOUT_COLUMN is defined for both GridLayout and TableLayout, - // so we don't want to do - // PARAM_TO_VIEW.put(ATTR_LAYOUT_COLUMN, TABLE_ROW); - // here since it would wipe out the above GridLayout registration. - // Since this is the only case where there is a conflict (in addition to layout_gravity - // which is defined in many places), rather than making the map point to lists - // this specific case is just special cased below, look for ATTR_LAYOUT_COLUMN. - PARAM_TO_VIEW.put(ATTR_LAYOUT_SPAN, TABLE_ROW); - - // Relative Layout - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_LEFT, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_RIGHT, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_TOP, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BOTTOM, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_TOP, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_BOTTOM, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_LEFT, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_PARENT_RIGHT, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_WITH_PARENT_MISSING, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ALIGN_BASELINE, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_IN_PARENT, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_VERTICAL, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_CENTER_HORIZONTAL, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_RIGHT_OF, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_TO_LEFT_OF, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_BELOW, RELATIVE_LAYOUT); - PARAM_TO_VIEW.put(ATTR_LAYOUT_ABOVE, RELATIVE_LAYOUT); - } - - /** - * Map from an included layout to all the including contexts (each including - * context is a pair of a file containing the include to the parent tag at - * the included location) - */ - private Map<String, List<Pair<File, String>>> mIncludes; - - /** - * List of pending include checks. When a layout parameter attribute is - * found on a root element, or on a child of a {@code merge} root tag, then - * we want to check across layouts whether the including context (the parent - * of the include tag) is valid for this attribute. We cannot check this - * immediately because we are processing the layouts in an arbitrary order - * so the included layout may be seen before the including layout and so on. - * Therefore, we stash these attributes to be checked after we're done. Each - * pair is a pair of an attribute name to be checked, and the file that - * attribute is referenced in. - */ - private final List<Pair<String, Location.Handle>> mPending = - new ArrayList<Pair<String,Location.Handle>>(); - - /** Constructs a new {@link ObsoleteLayoutParamsDetector} */ - public ObsoleteLayoutParamsDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(VIEW_INCLUDE); - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String name = attribute.getLocalName(); - if (name != null && name.startsWith(ATTR_LAYOUT_RESOURCE_PREFIX) - && ANDROID_URI.equals(attribute.getNamespaceURI())) { - if (VALID.contains(name)) { - return; - } - - String parent = PARAM_TO_VIEW.get(name); - if (parent != null) { - Element viewElement = attribute.getOwnerElement(); - Node layoutNode = viewElement.getParentNode(); - if (layoutNode == null || layoutNode.getNodeType() != Node.ELEMENT_NODE) { - // This is a layout attribute on a root element; this presumably means - // that this layout is included so check the included layouts to make - // sure at least one included context is valid for this layout_param. - // We can't do that yet since we may be processing the include tag to - // this layout after the layout itself. Instead, stash a work order... - if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { - IDomParser parser = context.parser; - Location.Handle handle = parser.createLocationHandle(context, attribute); - handle.setClientData(attribute); - mPending.add(Pair.of(name, handle)); - } - - return; - } - - String parentTag = ((Element) layoutNode).getTagName(); - if (parentTag.equals(VIEW_MERGE)) { - // This is a merge which means we need to check the including contexts, - // wherever they are. This has to be done after all the files have been - // scanned since we are not processing the files in any particular order. - if (context.getScope().contains(Scope.ALL_RESOURCE_FILES)) { - IDomParser parser = context.parser; - Location.Handle handle = parser.createLocationHandle(context, attribute); - handle.setClientData(attribute); - mPending.add(Pair.of(name, handle)); - } - - return; - } - - if (!isValidParamForParent(context, name, parent, parentTag)) { - if (name.equals(ATTR_LAYOUT_COLUMN) - && isValidParamForParent(context, name, TABLE_ROW, parentTag)) { - return; - } - context.report(ISSUE, attribute, context.getLocation(attribute), - String.format("Invalid layout param in a %1$s: %2$s", parentTag, name), - null); - } - } else { - // We could warn about unknown layout params but this might be brittle if - // new params are added or if people write custom ones; this is just a log - // for us to track these and update the check as necessary: - //context.client.log(null, - // String.format("Unrecognized layout param '%1$s'", name)); - } - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String layout = element.getAttribute(ATTR_LAYOUT); - if (layout.startsWith(LAYOUT_RESOURCE_PREFIX)) { // Ignore @android:layout/ layouts - layout = layout.substring(LAYOUT_RESOURCE_PREFIX.length()); - - Node parent = element.getParentNode(); - if (parent.getNodeType() == Node.ELEMENT_NODE) { - String tag = parent.getNodeName(); - if (tag.indexOf('.') == -1 && !tag.equals(VIEW_MERGE)) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - if (mIncludes == null) { - mIncludes = new HashMap<String, List<Pair<File, String>>>(); - } - List<Pair<File, String>> includes = mIncludes.get(layout); - if (includes == null) { - includes = new ArrayList<Pair<File, String>>(); - mIncludes.put(layout, includes); - } - includes.add(Pair.of(context.file, tag)); - } - } - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mIncludes == null) { - return; - } - - for (Pair<String, Location.Handle> pending : mPending) { - Handle handle = pending.getSecond(); - Location location = handle.resolve(); - File file = location.getFile(); - String layout = file.getName(); - if (layout.endsWith(DOT_XML)) { - layout = layout.substring(0, layout.length() - DOT_XML.length()); - } - - List<Pair<File, String>> includes = mIncludes.get(layout); - if (includes == null) { - // Nobody included this file - continue; - } - - String name = pending.getFirst(); - String parent = PARAM_TO_VIEW.get(name); - if (parent == null) { - continue; - } - - boolean isValid = false; - for (Pair<File, String> include : includes) { - String parentTag = include.getSecond(); - if (isValidParamForParent(context, name, parent, parentTag)) { - isValid = true; - break; - } else if (!isValid && name.equals(ATTR_LAYOUT_COLUMN) - && isValidParamForParent(context, name, TABLE_ROW, parentTag)) { - isValid = true; - break; - } - } - - if (!isValid) { - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ISSUE, (Node) clientData)) { - return; - } - } - - StringBuilder sb = new StringBuilder(40); - for (Pair<File, String> include : includes) { - if (sb.length() > 0) { - sb.append(", "); //$NON-NLS-1$ - } - File from = include.getFirst(); - String parentTag = include.getSecond(); - sb.append(String.format("included from within a %1$s in %2$s", - parentTag, - from.getParentFile().getName() + File.separator + from.getName())); - } - String message = String.format("Invalid layout param '%1$s' (%2$s)", - name, sb.toString()); - // TODO: Compute applicable scope node - context.report(ISSUE, location, message, null); - } - } - } - - /** - * Checks whether the given layout parameter name is valid for the given - * parent tag assuming it has the given current parent tag - */ - private static boolean isValidParamForParent(Context context, String name, String parent, - String parentTag) { - if (parentTag.indexOf('.') != -1 || parentTag.equals(VIEW_TAG)) { - // Custom tag: We don't know whether it extends one of the builtin - // types where the layout param is valid, so don't complain - return true; - } - - SdkInfo sdk = context.getSdkInfo(); - - if (!parentTag.equals(parent)) { - String tag = sdk.getParentViewName(parentTag); - while (tag != null) { - if (tag.equals(parent)) { - return true; - } - tag = sdk.getParentViewName(tag); - } - - return false; - } - - return true; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OnClickDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OnClickDetector.java deleted file mode 100644 index 2a07a86..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OnClickDetector.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright (C) 2012 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.ATTR_ON_CLICK; -import static com.android.SdkConstants.PREFIX_RESOURCE_REF; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -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.XmlContext; -import com.google.common.base.Joiner; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; -import org.w3c.dom.Attr; -import org.w3c.dom.Node; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Checks for missing onClick handlers - */ -public class OnClickDetector extends LayoutDetector implements ClassScanner { - /** Missing onClick handlers */ - public static final Issue ISSUE = Issue.create( - "OnClick", //$NON-NLS-1$ - "Ensures that onClick attribute values refer to real methods", - - "The `onClick` attribute value should be the name of a method in this View's context " + - "to invoke when the view is clicked. This name must correspond to a public method " + - "that takes exactly one parameter of type `View`.\n" + - "\n" + - "Must be a string value, using '\\;' to escape characters such as '\\n' or " + - "'\\uxxxx' for a unicode character.", - Category.CORRECTNESS, - 10, - Severity.ERROR, - OnClickDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.CLASS_FILE)); - - private Map<String, Location.Handle> mNames; - private Map<String, List<String>> mSimilar; - private boolean mHaveBytecode; - - /** Constructs a new {@link OnClickDetector} */ - public OnClickDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mNames != null && !mNames.isEmpty() && mHaveBytecode) { - List<String> names = new ArrayList<String>(mNames.keySet()); - Collections.sort(names); - LintDriver driver = context.getDriver(); - for (String name : names) { - Handle handle = mNames.get(name); - - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (driver.isSuppressed(ISSUE, (Node) clientData)) { - continue; - } - } - - Location location = handle.resolve(); - String message = String.format( - "Corresponding method handler 'public void %1$s(android.view.View)' not found", - name); - List<String> similar = mSimilar != null ? mSimilar.get(name) : null; - if (similar != null) { - Collections.sort(similar); - message += String.format(" (did you mean %1$s ?)", Joiner.on(", ").join(similar)); - } - context.report(ISSUE, location, message, null); - } - } - } - - // ---- Implements XmlScanner ---- - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_ON_CLICK); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String value = attribute.getValue(); - if (value.isEmpty() || value.trim().isEmpty()) { - context.report(ISSUE, attribute, context.getLocation(attribute), - "onClick attribute value cannot be empty", null); - } else if (!value.equals(value.trim())) { - context.report(ISSUE, attribute, context.getLocation(attribute), - "There should be no whitespace around attribute values", null); - } else if (!value.startsWith(PREFIX_RESOURCE_REF)) { // Not resolved - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - if (mNames == null) { - mNames = new HashMap<String, Location.Handle>(); - } - Handle handle = context.parser.createLocationHandle(context, attribute); - handle.setClientData(attribute); - - // Replace unicode characters with the actual value since that's how they - // appear in the ASM signatures - if (value.contains("\\u")) { //$NON-NLS-1$ - Pattern pattern = Pattern.compile("\\\\u(\\d\\d\\d\\d)"); //$NON-NLS-1$ - Matcher matcher = pattern.matcher(value); - StringBuilder sb = new StringBuilder(value.length()); - int remainder = 0; - while (matcher.find()) { - sb.append(value.substring(0, matcher.start())); - String unicode = matcher.group(1); - int hex = Integer.parseInt(unicode, 16); - sb.append((char) hex); - remainder = matcher.end(); - } - sb.append(value.substring(remainder)); - value = sb.toString(); - } - - mNames.put(value, handle); - } - } - - // ---- Implements ClassScanner ---- - - @SuppressWarnings("rawtypes") - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if (mNames == null) { - // No onClick attributes in the XML files - return; - } - - mHaveBytecode = true; - - List methodList = classNode.methods; - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - boolean rightArguments = method.desc.equals("(Landroid/view/View;)V"); //$NON-NLS-1$ - if (!mNames.containsKey(method.name)) { - if (rightArguments) { - // See if there's a possible typo instead - for (String n : mNames.keySet()) { - if (LintUtils.editDistance(n, method.name) <= 2) { - recordSimilar(n, classNode, method); - break; - } - } - } - continue; - } - - // TODO: Validate class hierarchy: should extend a context method - // Longer term, also validate that it's in a layout that corresponds to - // the given activity - - if (rightArguments){ - // Found: remove from list to be checked - mNames.remove(method.name); - - // Make sure the method is public - if ((method.access & Opcodes.ACC_PUBLIC) == 0) { - Location location = context.getLocation(method, classNode); - String message = String.format( - "On click handler %1$s(View) must be public", - method.name); - context.report(ISSUE, location, message, null); - } else if ((method.access & Opcodes.ACC_STATIC) != 0) { - Location location = context.getLocation(method, classNode); - String message = String.format( - "On click handler %1$s(View) should not be static", - method.name); - context.report(ISSUE, location, message, null); - } - - if (mNames.isEmpty()) { - mNames = null; - return; - } - } - } - } - - private void recordSimilar(String name, ClassNode classNode, MethodNode method) { - if (mSimilar == null) { - mSimilar = new HashMap<String, List<String>>(); - } - List<String> list = mSimilar.get(name); - if (list == null) { - list = new ArrayList<String>(); - mSimilar.put(name, list); - } - - String signature = ClassContext.createSignature(classNode.name, method.name, method.desc); - list.add(signature); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverdrawDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverdrawDetector.java deleted file mode 100644 index 6627411..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverdrawDetector.java +++ /dev/null @@ -1,566 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BACKGROUND; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PARENT; -import static com.android.SdkConstants.ATTR_THEME; -import static com.android.SdkConstants.ATTR_TILE_MODE; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.DRAWABLE_PREFIX; -import static com.android.SdkConstants.NULL_RESOURCE; -import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_APPLICATION; -import static com.android.SdkConstants.TAG_BITMAP; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.SdkConstants.TRANSPARENT_COLOR; -import static com.android.SdkConstants.VALUE_DISABLED; -import static com.android.tools.lint.detector.api.LintUtils.endsWith; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -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.XmlContext; -import com.android.utils.Pair; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import lombok.ast.AstVisitor; -import lombok.ast.ClassDeclaration; -import lombok.ast.CompilationUnit; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.MethodInvocation; -import lombok.ast.PackageDeclaration; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.VariableReference; - -/** - * Check which looks for overdraw problems where view areas are painted and then - * painted over, meaning that the bottom paint operation is a waste of time. - */ -public class OverdrawDetector extends LayoutDetector implements Detector.JavaScanner { - private static final String R_STYLE_PREFIX = "R.style."; //$NON-NLS-1$ - private static final String SET_THEME = "setTheme"; //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "Overdraw", //$NON-NLS-1$ - "Looks for overdraw issues (where a view is painted only to be fully painted over)", - "If you set a background drawable on a root view, then you should use a " + - "custom theme where the theme background is null. Otherwise, the theme background " + - "will be painted first, only to have your custom background completely cover it; " + - "this is called \"overdraw\".\n" + - "\n" + - "NOTE: This detector relies on figuring out which layouts are associated with " + - "which activities based on scanning the Java code, and it's currently doing that " + - "using an inexact pattern matching algorithm. Therefore, it can incorrectly " + - "conclude which activity the layout is associated with and then wrongly complain " + - "that a background-theme is hidden.\n" + - "\n" + - "If you want your custom background on multiple pages, then you should consider " + - "making a custom theme with your custom background and just using that theme " + - "instead of a root element background.\n" + - "\n" + - "Of course it's possible that your custom drawable is translucent and you want " + - "it to be mixed with the background. However, you will get better performance " + - "if you pre-mix the background with your drawable and use that resulting image or " + - "color as a custom theme background instead.\n", - - Category.PERFORMANCE, - 3, - Severity.WARNING, - OverdrawDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES)); - - /** Mapping from FQN activity names to theme names registered in the manifest */ - private Map<String, String> mActivityToTheme; - - /** The default theme declared in the manifest, or null */ - private String mManifestTheme; - - /** Mapping from layout name (not including {@code @layout/} prefix) to activity FQN */ - private Map<String, List<String>> mLayoutToActivity; - - /** List of theme names registered in the project which have blank backgrounds */ - private List<String> mBlankThemes; - - /** Set of activities registered in the manifest. We will limit the Java analysis to - * these. */ - private Set<String> mActivities; - - /** List of drawable resources that are not flagged for overdraw (XML drawables - * except for {@code <bitmap>} drawables without tiling) */ - private List<String> mValidDrawables; - - /** - * List of pairs of (location, background drawable) corresponding to root elements - * in layouts that define a given background drawable. These should be checked to - * see if they are painting on top of a non-transparent theme. - */ - private List<Pair<Location, String>> mRootAttributes; - - /** Constructs a new {@link OverdrawDetector} */ - public OverdrawDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - // Look in layouts for drawable resources - return super.appliesTo(folderType) - // and in resource files for theme definitions - || folderType == ResourceFolderType.VALUES - // and in drawable files for bitmap tiling modes - || folderType == ResourceFolderType.DRAWABLE; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return LintUtils.isXmlFile(file) || LintUtils.endsWith(file.getName(), DOT_JAVA); - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - /** Is the given theme a "blank" theme (one not painting its background) */ - private boolean isBlankTheme(String name) { - if (name.startsWith("@android:style/Theme_")) { //$NON-NLS-1$ - if (name.contains("NoFrame") //$NON-NLS-1$ - || name.contains("Theme_Wallpaper") //$NON-NLS-1$ - || name.contains("Theme_Holo_Wallpaper") //$NON-NLS-1$ - || name.contains("Theme_Translucent") //$NON-NLS-1$ - || name.contains("Theme_Dialog_NoFrame") //$NON-NLS-1$ - || name.contains("Theme_Holo_Dialog_Alert") //$NON-NLS-1$ - || name.contains("Theme_Holo_Light_Dialog_Alert") //$NON-NLS-1$ - || name.contains("Theme_Dialog_Alert") //$NON-NLS-1$ - || name.contains("Theme_Panel") //$NON-NLS-1$ - || name.contains("Theme_Light_Panel") //$NON-NLS-1$ - || name.contains("Theme_Holo_Panel") //$NON-NLS-1$ - || name.contains("Theme_Holo_Light_Panel")) { //$NON-NLS-1$ - return true; - } - } - - if (mBlankThemes != null && mBlankThemes.contains(name)) { - return true; - } - - return false; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mRootAttributes != null) { - for (Pair<Location, String> pair : mRootAttributes) { - Location location = pair.getFirst(); - - Object clientData = location.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ISSUE, (Node) clientData)) { - return; - } - } - - String layoutName = location.getFile().getName(); - if (endsWith(layoutName, DOT_XML)) { - layoutName = layoutName.substring(0, layoutName.length() - DOT_XML.length()); - } - - String theme = getTheme(context, layoutName); - if (theme == null || !isBlankTheme(theme)) { - String drawable = pair.getSecond(); - String message = String.format( - "Possible overdraw: Root element paints background %1$s with " + - "a theme that also paints a background (inferred theme is %2$s)", - drawable, theme); - // TODO: Compute applicable scope node - context.report(ISSUE, location, message, null); - } - - } - } - } - - /** Return the theme to be used for the given layout */ - private String getTheme(Context context, String layoutName) { - if (mActivityToTheme != null && mLayoutToActivity != null) { - List<String> activities = mLayoutToActivity.get(layoutName); - if (activities != null) { - for (String activity : activities) { - String theme = mActivityToTheme.get(activity); - if (theme != null) { - return theme; - } - } - } - } - - if (mManifestTheme != null) { - return mManifestTheme; - } - - Project project = context.getMainProject(); - int apiLevel = project.getTargetSdk(); - if (apiLevel == -1) { - apiLevel = project.getMinSdk(); - } - - if (apiLevel >= 11) { - return "@android:style/Theme.Holo"; //$NON-NLS-1$ - } else { - return "@android:style/Theme"; //$NON-NLS-1$ - } - } - - // ---- Implements XmlScanner ---- - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - // Only consider the root element's background - if (attribute.getOwnerDocument().getDocumentElement() == attribute.getOwnerElement()) { - // If the drawable is a non-repeated pattern then the overdraw might be - // intentional since the image isn't covering the whole screen - String background = attribute.getValue(); - if (mValidDrawables != null && mValidDrawables.contains(background)) { - return; - } - - if (background.equals(TRANSPARENT_COLOR)) { - return; - } - - if (background.startsWith("@android:drawable/")) { //$NON-NLS-1$ - // We haven't had a chance to study the builtin drawables the way we - // check the project local ones in scanBitmap() and beforeCheckFile(), - // but many of these are not bitmaps, so ignore these - return; - } - - String name = context.file.getName(); - if (name.contains("list_") || name.contains("_item")) { //$NON-NLS-1$ //$NON-NLS-2$ - // Canonical list_item layout name: don't warn about these, it's - // pretty common to want to paint custom list item backgrounds - return; - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - Location location = context.getLocation(attribute); - location.setClientData(attribute); - if (mRootAttributes == null) { - mRootAttributes = new ArrayList<Pair<Location,String>>(); - } - mRootAttributes.add(Pair.of(location, attribute.getValue())); - } - } - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList( - // Layouts: Look for background attributes on root elements for possible overdraw - ATTR_BACKGROUND - ); - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - // Manifest: Look at theme registrations - TAG_ACTIVITY, - TAG_APPLICATION, - - // Resource files: Look at theme definitions - TAG_STYLE, - - // Bitmaps - TAG_BITMAP - ); - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - if (endsWith(context.file.getName(), DOT_XML)) { - // Drawable XML files should not be considered for overdraw, except for <bitmap>'s. - // The bitmap elements are handled in the scanBitmap() method; it will clear - // out anything added by this method. - File parent = context.file.getParentFile(); - ResourceFolderType type = ResourceFolderType.getFolderType(parent.getName()); - if (type == ResourceFolderType.DRAWABLE) { - if (mValidDrawables == null) { - mValidDrawables = new ArrayList<String>(); - } - String resource = getDrawableResource(context.file); - mValidDrawables.add(resource); - } - } - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - if (tag.equals(TAG_STYLE)) { - scanTheme(element); - } else if (tag.equals(TAG_ACTIVITY)) { - scanActivity(context, element); - } else if (tag.equals(TAG_APPLICATION)) { - if (element.hasAttributeNS(ANDROID_URI, ATTR_THEME)) { - mManifestTheme = element.getAttributeNS(ANDROID_URI, ATTR_THEME); - } - } else if (tag.equals(TAG_BITMAP)) { - scanBitmap(context, element); - } - } - - private static String getDrawableResource(File drawableFile) { - String resource = drawableFile.getName(); - if (endsWith(resource, DOT_XML)) { - resource = resource.substring(0, resource.length() - DOT_XML.length()); - } - return DRAWABLE_PREFIX + resource; - } - - private void scanBitmap(Context context, Element element) { - String tileMode = element.getAttributeNS(ANDROID_URI, ATTR_TILE_MODE); - if (!(tileMode.equals(VALUE_DISABLED) || tileMode.isEmpty())) { - if (mValidDrawables != null) { - String resource = getDrawableResource(context.file); - mValidDrawables.remove(resource); - } - } - } - - private void scanActivity(Context context, Element element) { - String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); - if (name.indexOf('$') != -1) { - name = name.replace('$', '.'); - } - if (name.startsWith(".")) { //$NON-NLS-1$ - String pkg = context.getProject().getPackage(); - if (pkg != null && !pkg.isEmpty()) { - name = pkg + name; - } - } - - if (mActivities == null) { - mActivities = new HashSet<String>(); - } - mActivities.add(name); - - String theme = element.getAttributeNS(ANDROID_URI, ATTR_THEME); - if (theme != null && !theme.isEmpty()) { - if (mActivityToTheme == null) { - mActivityToTheme = new HashMap<String, String>(); - } - mActivityToTheme.put(name, theme.replace('.', '_')); - } - } - - private void scanTheme(Element element) { - // Look for theme definitions, and record themes that provide a null background. - String styleName = element.getAttribute(ATTR_NAME); - String parent = element.getAttribute(ATTR_PARENT); - if (parent == null) { - // Eclipse DOM workaround - parent = ""; - } - - if (parent.isEmpty()) { - int index = styleName.lastIndexOf('.'); - if (index != -1) { - parent = styleName.substring(0, index); - } - } - parent = parent.replace('.', '_'); - - String resource = STYLE_RESOURCE_PREFIX + styleName.replace('.', '_'); - - NodeList items = element.getChildNodes(); - for (int i = 0, n = items.getLength(); i < n; i++) { - if (items.item(i).getNodeType() == Node.ELEMENT_NODE) { - Element item = (Element) items.item(i); - String name = item.getAttribute(ATTR_NAME); - if (name.equals("android:windowBackground")) { //$NON-NLS-1$ - NodeList textNodes = item.getChildNodes(); - for (int j = 0, m = textNodes.getLength(); j < m; j++) { - Node textNode = textNodes.item(j); - if (textNode.getNodeType() == Node.TEXT_NODE) { - String text = textNode.getNodeValue(); - String trim = text.trim(); - if (!trim.isEmpty()) { - if (trim.equals(NULL_RESOURCE) - || trim.equals(TRANSPARENT_COLOR) - || mValidDrawables != null - && mValidDrawables.contains(trim)) { - if (mBlankThemes == null) { - mBlankThemes = new ArrayList<String>(); - } - mBlankThemes.add(resource); - } - } - } - } - - return; - } - } - } - - if (isBlankTheme(parent)) { - if (mBlankThemes == null) { - mBlankThemes = new ArrayList<String>(); - } - mBlankThemes.add(resource); - } - } - - // ---- Implements JavaScanner ---- - - @Override - public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() { - // This detector does not specify specific node types; this means - // that the infrastructure will run the full visitor on the compilation - // unit rather than on individual nodes. This is important since this - // detector relies on pruning (if it gets to a class declaration that is - // not an activity, it skips everything inside). - return null; - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - if (!context.getProject().getReportIssues()) { - return null; - } - return new OverdrawVisitor(); - } - - private class OverdrawVisitor extends ForwardingAstVisitor { - private static final String ACTIVITY = "Activity"; //$NON-NLS-1$ - private String mClassFqn; - - @Override - public boolean visitClassDeclaration(ClassDeclaration node) { - String name = node.getDescription(); - - if (mActivities != null && mActivities.contains(mClassFqn) || name.endsWith(ACTIVITY) - || node.astExtending() != null && - node.astExtending().getDescription().endsWith(ACTIVITY)) { - String packageName = ""; - if (node.getParent() instanceof CompilationUnit) { - CompilationUnit compilationUnit = (CompilationUnit) node.getParent(); - PackageDeclaration packageDeclaration = compilationUnit.astPackageDeclaration(); - if (packageDeclaration == null) { - // No package declaration: ignore this one - return true; - } - packageName = packageDeclaration.getPackageName(); - } - mClassFqn = (!packageName.isEmpty() ? (packageName + '.') : "") + name; - - return false; - } - - return true; // Done: No need to look inside this class - } - - // Store R.layout references in activity classes in a map mapping back layouts - // to activities - @Override - public boolean visitSelect(Select node) { - if (node.astIdentifier().astValue().equals("layout") //$NON-NLS-1$ - && node.astOperand() instanceof VariableReference - && ((VariableReference) node.astOperand()).astIdentifier().astValue() - .equals("R") //$NON-NLS-1$ - && node.getParent() instanceof Select) { - String layout = ((Select) node.getParent()).astIdentifier().astValue(); - if (mLayoutToActivity == null) { - mLayoutToActivity = new HashMap<String, List<String>>(); - } - List<String> list = mLayoutToActivity.get(layout); - if (list == null) { - list = new ArrayList<String>(); - mLayoutToActivity.put(layout, list); - } - list.add(mClassFqn); - } - - return false; - } - - - // Look for setTheme(R.style.whatever) and register as a theme registration - // for the current activity - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (node.astName().astValue().equals(SET_THEME)) { - // Look at argument - StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); - if (args.size() == 1) { - Expression arg = args.first(); - if (arg instanceof Select) { - String resource = arg.toString(); - if (resource.startsWith(R_STYLE_PREFIX)) { - if (mActivityToTheme == null) { - mActivityToTheme = new HashMap<String, String>(); - } - String name = ((Select) arg).astIdentifier().astValue(); - mActivityToTheme.put(mClassFqn, STYLE_RESOURCE_PREFIX + name); - } - } - } - } - - return false; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverrideDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverrideDetector.java deleted file mode 100644 index 15e2245..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/OverrideDetector.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2012 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.CONSTRUCTOR_NAME; -import static org.objectweb.asm.Opcodes.ACC_PRIVATE; -import static org.objectweb.asm.Opcodes.ACC_PROTECTED; -import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import static org.objectweb.asm.Opcodes.ACC_STATIC; - -import com.android.annotations.NonNull; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -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.Maps; -import com.google.common.collect.Sets; -import com.google.common.collect.Sets.SetView; - -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.EnumSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -/** - * Checks for accidental overrides - */ -public class OverrideDetector extends Detector implements ClassScanner { - /** Accidental overrides */ - public static final Issue ISSUE = Issue.create( - "DalvikOverride", //$NON-NLS-1$ - "Looks for methods treated as overrides by Dalvik", - - "The Android virtual machine will treat a package private method in one " + - "class as overriding a package private method in its super class, even if " + - "they are in separate packages. This may be surprising, but for compatibility " + - "reasons the behavior has not been changed (yet).\n" + - "\n" + - "If you really did intend for this method to override the other, make the " + - "method `protected` instead.\n" + - "\n" + - "If you did *not* intend the override, consider making the method private, or " + - "changing its name or signature.", - - Category.CORRECTNESS, - 7, - Severity.ERROR, - OverrideDetector.class, - EnumSet.of(Scope.ALL_CLASS_FILES)); - - /** map from owner class name to JVM signatures for its package private methods */ - private final Map<String, Set<String>> mPackagePrivateMethods = Maps.newHashMap(); - - /** Map from owner to signature to super class being overridden */ - private Map<String, Map<String, String>> mErrors; - - /** - * Map from owner to signature to corresponding location. When there are - * errors a single error can have locations for both the overriding and - * overridden methods. - */ - private Map<String, Map<String, Location>> mLocations; - - /** Constructs a new {@link OverrideDetector} */ - public OverrideDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - // Process the check in two passes: - // - // In the first pass, gather the full set of package private methods for - // each class. - // When all classes have been processed at the end of the first pass, - // find out whether any of the methods are potentially overriding those - // in its super classes. - // - // If so, request a second pass. In the second pass, we gather full locations - // for both the base and overridden method calls, and store these. - // If the location is found to be in a suppressed context, remove that error - // entry. - // - // At the end of the second pass, we generate the errors, combining locations - // from both the overridden and overriding methods. - if (context.getPhase() == 1) { - Set<String> classes = mPackagePrivateMethods.keySet(); - LintDriver driver = context.getDriver(); - for (String owner : classes) { - Set<String> methods = mPackagePrivateMethods.get(owner); - String superClass = driver.getSuperClass(owner); - int packageIndex = owner.lastIndexOf('/'); - while (superClass != null) { - int superPackageIndex = superClass.lastIndexOf('/'); - - // Only compare methods that differ in packages - if (packageIndex == -1 || superPackageIndex != packageIndex || - !owner.regionMatches(0, superClass, 0, packageIndex)) { - Set<String> superMethods = mPackagePrivateMethods.get(superClass); - if (superMethods != null) { - SetView<String> intersection = Sets.intersection(methods, - superMethods); - if (!intersection.isEmpty()) { - if (mLocations == null) { - mLocations = Maps.newHashMap(); - } - // We need a separate data structure to keep track of which - // signatures are in error, - if (mErrors == null) { - mErrors = Maps.newHashMap(); - } - - for (String signature : intersection) { - Map<String, Location> locations = mLocations.get(owner); - if (locations == null) { - locations = Maps.newHashMap(); - mLocations.put(owner, locations); - } - locations.put(signature, null); - - locations = mLocations.get(superClass); - if (locations == null) { - locations = Maps.newHashMap(); - mLocations.put(superClass, locations); - } - locations.put(signature, null); - - - Map<String, String> errors = mErrors.get(owner); - if (errors == null) { - errors = Maps.newHashMap(); - mErrors.put(owner, errors); - } - errors.put(signature, superClass); - } - } - } - } - superClass = driver.getSuperClass(superClass); - } - } - - if (mErrors != null) { - context.requestRepeat(this, ISSUE.getScope()); - } - } else { - assert context.getPhase() == 2; - - for (Entry<String, Map<String, String>> ownerEntry : mErrors.entrySet()) { - String owner = ownerEntry.getKey(); - Map<String, String> methodToSuper = ownerEntry.getValue(); - for (Entry<String, String> entry : methodToSuper.entrySet()) { - String signature = entry.getKey(); - String superClass = entry.getValue(); - - Map<String, Location> ownerLocations = mLocations.get(owner); - if (ownerLocations != null) { - Location location = ownerLocations.get(signature); - if (location != null) { - Map<String, Location> superLocations = mLocations.get(superClass); - if (superLocations != null) { - Location superLocation = superLocations.get(signature); - if (superLocation != null) { - location.setSecondary(superLocation); - superLocation.setMessage( - "This method is treated as overridden"); - } - } - String methodName = signature; - int index = methodName.indexOf('('); - if (index != -1) { - methodName = methodName.substring(0, index); - } - String message = String.format( - "This package private method may be unintentionally " + - "overriding %1$s in %2$s", methodName, - ClassContext.getFqcn(superClass)); - context.report(ISSUE, location, message, null); - } - } - } - } - } - } - - @SuppressWarnings("rawtypes") // ASM4 API - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - List methodList = classNode.methods; - if (context.getPhase() == 1) { - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - int access = method.access; - // Only record non-static package private methods - if ((access & (ACC_STATIC|ACC_PRIVATE|ACC_PROTECTED|ACC_PUBLIC)) != 0) { - continue; - } - - // Ignore constructors too - if (CONSTRUCTOR_NAME.equals(method.name)) { - continue; - } - - String owner = classNode.name; - Set<String> methods = mPackagePrivateMethods.get(owner); - if (methods == null) { - methods = Sets.newHashSetWithExpectedSize(methodList.size()); - mPackagePrivateMethods.put(owner, methods); - } - methods.add(method.name + method.desc); - } - } else { - assert context.getPhase() == 2; - Map<String, Location> methods = mLocations.get(classNode.name); - if (methods == null) { - // No locations needed from this class - return; - } - - for (Object m : methodList) { - MethodNode method = (MethodNode) m; - - String signature = method.name + method.desc; - if (methods.containsKey(signature)){ - if (context.getDriver().isSuppressed(ISSUE, classNode, - method, null)) { - Map<String, String> errors = mErrors.get(classNode.name); - if (errors != null) { - errors.remove(signature); - } - continue; - } - - Location location = context.getLocation(method, classNode); - methods.put(signature, location); - String description = ClassContext.createSignature(classNode.name, - method.name, method.desc); - location.setClientData(description); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateKeyDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateKeyDetector.java deleted file mode 100644 index e8887a5..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateKeyDetector.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -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.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.google.common.base.Charsets; -import com.google.common.io.Files; - -import java.io.File; -import java.io.IOException; -import java.util.EnumSet; - -/** - * Looks for packaged private key files. - */ -public class PrivateKeyDetector extends Detector implements Detector.OtherFileScanner { - /** Packaged private key files */ - public static final Issue ISSUE = Issue.create( - "PackagedPrivateKey", //$NON-NLS-1$ - "Looks for packaged private key files", - - "In general, you should not package private key files inside your app.", - - Category.SECURITY, - 8, - Severity.WARNING, - PrivateKeyDetector.class, - Scope.OTHER_SCOPE); - - /** Constructs a new {@link PrivateKeyDetector} check */ - public PrivateKeyDetector() { - } - - private static boolean isPrivateKeyFile(File file) { - if (!file.isFile() || - (!LintUtils.endsWith(file.getPath(), "pem") && //NON-NLS-1$ - !LintUtils.endsWith(file.getPath(), "key"))) { //NON-NLS-1$ - return false; - } - - try { - String firstLine = Files.readFirstLine(file, Charsets.US_ASCII); - return firstLine != null && - firstLine.startsWith("---") && //NON-NLS-1$ - firstLine.contains("PRIVATE KEY"); //NON-NLS-1$ - } catch (IOException ex) { - // Don't care - } - - return false; - } - - // ---- Implements OtherFileScanner ---- - - @NonNull - @Override - public EnumSet<Scope> getApplicableFiles() { - return Scope.OTHER_SCOPE; - } - - @Override - public void run(@NonNull Context context) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - File file = context.file; - if (isPrivateKeyFile(file)) { - String fileName = file.getParentFile().getName() + File.separator - + file.getName(); - String message = String.format( - "The %1$s file seems to be a private key file. " + - "Please make sure not to embed this in your APK file.", fileName); - context.report(ISSUE, Location.create(file), message, null); - } - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateResourceDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateResourceDetector.java deleted file mode 100644 index d8a4ce0..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PrivateResourceDetector.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Attr; - -import java.util.Collection; - -/** - * Check which looks for access of private resources. - */ -public class PrivateResourceDetector extends ResourceXmlDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "PrivateResource", //$NON-NLS-1$ - "Looks for references to private resources", - "Private resources should not be referenced; the may not be present everywhere, and " + - "even where they are they may disappear without notice.\n" + - "\n" + - "To fix this, copy the resource into your own project. You can find the platform " + - "resources under `$ANDROID_SK/platforms/android-$VERSION/data/res/.`", - Category.CORRECTNESS, - 3, - Severity.FATAL, - PrivateResourceDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new detector */ - public PrivateResourceDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String value = attribute.getNodeValue(); - if (value.startsWith("@*android:")) { //$NON-NLS-1$ - context.report(ISSUE, attribute, context.getLocation(attribute), - "Illegal resource reference: @*android resources are private and " + - "not always present", null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ProguardDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ProguardDetector.java deleted file mode 100644 index 7762659..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ProguardDetector.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.PROGUARD_CONFIG; -import static com.android.SdkConstants.PROJECT_PROPERTIES; - -import com.android.annotations.NonNull; -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.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 java.io.File; -import java.util.EnumSet; - -/** - * Check which looks for errors in Proguard files. - */ -public class ProguardDetector extends Detector { - - /** The main issue discovered by this detector */ - public static final Issue WRONGKEEP = Issue.create( - "Proguard", //$NON-NLS-1$ - "Looks for problems in proguard config files", - "Using `-keepclasseswithmembernames` in a proguard config file is not " + - "correct; it can cause some symbols to be renamed which should not be.\n" + - "Earlier versions of ADT used to create proguard.cfg files with the " + - "wrong format. Instead of `-keepclasseswithmembernames` use " + - "`-keepclasseswithmembers`, since the old flags also implies " + - "\"allow shrinking\" which means symbols only referred to from XML and " + - "not Java (such as possibly CustomViews) can get deleted.", - Category.CORRECTNESS, - 8, - Severity.FATAL, - ProguardDetector.class, - EnumSet.of(Scope.PROGUARD_FILE)).setMoreInfo( - "http://http://code.google.com/p/android/issues/detail?id=16384"); //$NON-NLS-1$ - - /** Finds ProGuard files that contain non-project specific configuration - * locally and suggests replacing it with an include path */ - public static final Issue SPLITCONFIG = Issue.create( - "ProguardSplit", //$NON-NLS-1$ - "Checks for old proguard.cfg files that contain generic Android rules", - - "Earlier versions of the Android tools bundled a single `proguard.cfg` file " + - "containing a ProGuard configuration file suitable for Android shrinking and " + - "obfuscation. However, that version was copied into new projects, which " + - "means that it does not continue to get updated as we improve the default " + - "ProGuard rules for Android.\n" + - "\n" + - "In the new version of the tools, we have split the ProGuard configuration " + - "into two halves:\n" + - "* A simple configuration file containing only project-specific flags, in " + - "your project\n" + - "* A generic configuration file containing the recommended set of ProGuard " + - "options for Android projects. This generic file lives in the SDK install " + - "directory which means that it gets updated along with the tools.\n" + - "\n" + - "In order for this to work, the proguard.config property in the " + - "`project.properties` file now refers to a path, so you can reference both " + - "the generic file as well as your own (and any additional files too).\n" + - "\n" + - "To migrate your project to the new setup, create a new `proguard-project.txt` file " + - "in your project containing any project specific ProGuard flags as well as " + - "any customizations you have made, then update your project.properties file " + - "to contain:\n" + - "`proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt`", - - Category.CORRECTNESS, - 3, - Severity.WARNING, - ProguardDetector.class, - EnumSet.of(Scope.PROGUARD_FILE)); - - @Override - public void run(@NonNull Context context) { - String contents = context.getContents(); - if (contents != null) { - if (context.isEnabled(WRONGKEEP)) { - int index = contents.indexOf( - // Old pattern: - "-keepclasseswithmembernames class * {\n" + //$NON-NLS-1$ - " public <init>(android."); //$NON-NLS-1$ - if (index != -1) { - context.report(WRONGKEEP, - Location.create(context.file, contents, index, index), - "Obsolete ProGuard file; use -keepclasseswithmembers instead of " + - "-keepclasseswithmembernames", null); - } - } - if (context.isEnabled(SPLITCONFIG)) { - int index = contents.indexOf("-keep public class * extends android.app.Activity"); - if (index != -1) { - // Only complain if project.properties actually references this file; - // no need to bother the users who got a default proguard.cfg file - // when they created their projects but haven't actually hooked it up - // to shrinking & obfuscation. - File propertyFile = new File(context.file.getParentFile(), PROJECT_PROPERTIES); - if (!propertyFile.exists()) { - return; - } - String properties = context.getClient().readFile(propertyFile); - int i = properties.indexOf(PROGUARD_CONFIG); - if (i == -1) { - return; - } - // Make sure the entry isn't just commented out, such as - // # To enable ProGuard to shrink and obfuscate your code, uncomment this: - // #proguard.config=proguard.cfg - for (; i >= 0; i--) { - char c = properties.charAt(i); - if (c == '#') { - return; - } - if (c == '\n') { - break; - } - } - if (properties.contains(PROGUARD_CONFIG)) { - context.report(SPLITCONFIG, - Location.create(context.file, contents, index, index), - String.format( - "Local ProGuard configuration contains general Android " + - "configuration: Inherit these settings instead? " + - "Modify project.properties to define " + - "proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:%1$s" + - " and then keep only project-specific configuration here", - context.file.getName()), null); - } - } - } - } - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PxUsageDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PxUsageDetector.java deleted file mode 100644 index ed9447e..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/PxUsageDetector.java +++ /dev/null @@ -1,297 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_TEXT_SIZE; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.SdkConstants.UNIT_DIP; -import static com.android.SdkConstants.UNIT_DP; -import static com.android.SdkConstants.UNIT_IN; -import static com.android.SdkConstants.UNIT_MM; -import static com.android.SdkConstants.UNIT_PX; -import static com.android.SdkConstants.UNIT_SP; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.Collection; -import java.util.Collections; - -/** - * Check for px dimensions instead of dp dimensions. - * Also look for non-"sp" text sizes. - */ -public class PxUsageDetector extends LayoutDetector { - /** Using px instead of dp */ - public static final Issue PX_ISSUE = Issue.create( - "PxUsage", //$NON-NLS-1$ - "Looks for use of the \"px\" dimension", - // This description is from the below screen support document - "For performance reasons and to keep the code simpler, the Android system uses pixels " + - "as the standard unit for expressing dimension or coordinate values. That means that " + - "the dimensions of a view are always expressed in the code using pixels, but " + - "always based on the current screen density. For instance, if `myView.getWidth()` " + - "returns 10, the view is 10 pixels wide on the current screen, but on a device with " + - "a higher density screen, the value returned might be 15. If you use pixel values " + - "in your application code to work with bitmaps that are not pre-scaled for the " + - "current screen density, you might need to scale the pixel values that you use in " + - "your code to match the un-scaled bitmap source.", - Category.CORRECTNESS, - 2, - Severity.WARNING, - PxUsageDetector.class, - Scope.RESOURCE_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/screens_support.html#screen-independence"); //$NON-NLS-1$ - - /** Using mm/in instead of dp */ - public static final Issue IN_MM_ISSUE = Issue.create( - "InOrMmUsage", //$NON-NLS-1$ - "Looks for use of the \"mm\" or \"in\" dimensions", - - "Avoid using `mm` (millimeters) or `in` (inches) as the unit for dimensions.\n" + - "\n" + - "While it should work in principle, unfortunately many devices do not report " + - "the correct true physical density, which means that the dimension calculations " + - "won't work correctly. You are better off using `dp` (and for font sizes, `sp`.)", - - Category.CORRECTNESS, - 4, - Severity.WARNING, - PxUsageDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Using sp instead of dp */ - public static final Issue DP_ISSUE = Issue.create( - "SpUsage", //$NON-NLS-1$ - "Looks for uses of \"dp\" instead of \"sp\" dimensions for text sizes", - - "When setting text sizes, you should normally use `sp`, or \"scale-independent " + - "pixels\". This is like the `dp` unit, but it is also scaled " + - "by the user's font size preference. It is recommend you use this unit when " + - "specifying font sizes, so they will be adjusted for both the screen density " + - "and the user's preference.\n" + - "\n" + - "There *are* cases where you might need to use `dp`; typically this happens when " + - "the text is in a container with a specific dp-size. This will prevent the text " + - "from spilling outside the container. Note however that this means that the user's " + - "font size settings are not respected, so consider adjusting the layout itself " + - "to be more flexible.", - Category.CORRECTNESS, - 3, - Severity.WARNING, - PxUsageDetector.class, - Scope.RESOURCE_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/training/multiscreen/screendensities.html"); //$NON-NLS-1$ - - /** Using text sizes that are too small */ - public static final Issue SMALL_SP_ISSUE = Issue.create( - "SmallSp", //$NON-NLS-1$ - "Looks for text sizes that are too small", - - "Avoid using sizes smaller than 12sp.", - - Category.USABILITY, - 4, - Severity.WARNING, - PxUsageDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - - /** Constructs a new {@link PxUsageDetector} */ - public PxUsageDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - // Look in both layouts (at attribute values) and in value files (at style definitions) - return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES; - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - @Nullable - public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_STYLE); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - if (context.getResourceFolderType() != ResourceFolderType.LAYOUT) { - return; - } - - String value = attribute.getValue(); - if (value.endsWith(UNIT_PX) && value.matches("\\d+px")) { //$NON-NLS-1$ - if (value.charAt(0) == '0') { - // 0px is fine. 0px is 0dp regardless of density... - return; - } - if (context.isEnabled(PX_ISSUE)) { - context.report(PX_ISSUE, attribute, context.getLocation(attribute), - "Avoid using \"px\" as units; use \"dp\" instead", null); - } - } else if (value.endsWith(UNIT_MM) && value.matches("\\d+mm") //$NON-NLS-1$ - || value.endsWith(UNIT_IN) && value.matches("\\d+in")) { //$NON-NLS-1$ - if (value.charAt(0) == '0') { - // 0mm == 0in == 0dp - return; - } - if (context.isEnabled(IN_MM_ISSUE)) { - String unit = value.substring(value.length() - 2); - context.report(IN_MM_ISSUE, attribute, context.getLocation(attribute), - String.format("Avoid using \"%1$s\" as units " + - "(it does not work accurately on all devices); use \"dp\" instead", - unit), - null); - } - } else if (value.endsWith(UNIT_SP) - && (ATTR_TEXT_SIZE.equals(attribute.getLocalName()) - || ATTR_LAYOUT_HEIGHT.equals(attribute.getLocalName())) - && value.matches("\\d+sp")) { //$NON-NLS-1$ - int size = getSize(value); - if (size > 0 && size < 12) { - context.report(SMALL_SP_ISSUE, attribute, context.getLocation(attribute), - String.format("Avoid using sizes smaller than 12sp: %1$s", value), - null); - } - } else if (ATTR_TEXT_SIZE.equals(attribute.getLocalName()) - && (value.endsWith(UNIT_DP) || value.endsWith(UNIT_DIP)) - && (value.matches("\\d+di?p"))) { //$NON-NLS-1$ - if (context.isEnabled(DP_ISSUE)) { - context.report(DP_ISSUE, attribute, context.getLocation(attribute), - "Should use \"sp\" instead of \"dp\" for text sizes", null); - } - } - } - - private static int getSize(String text) { - assert text.matches("\\d+sp") : text; //$NON-NLS-1$ - return Integer.parseInt(text.substring(0, text.length() - 2)); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (context.getResourceFolderType() != ResourceFolderType.VALUES) { - return; - } - - assert element.getTagName().equals(TAG_STYLE); - NodeList itemNodes = element.getChildNodes(); - for (int j = 0, nodeCount = itemNodes.getLength(); j < nodeCount; j++) { - Node item = itemNodes.item(j); - if (item.getNodeType() == Node.ELEMENT_NODE && - TAG_ITEM.equals(item.getNodeName())) { - Element itemElement = (Element) item; - NodeList childNodes = item.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() != Node.TEXT_NODE) { - return; - } - - checkStyleItem(context, itemElement, child); - } - } - } - } - - private static void checkStyleItem(XmlContext context, Element item, Node textNode) { - String text = textNode.getNodeValue(); - for (int j = text.length() - 1; j > 0; j--) { - char c = text.charAt(j); - if (!Character.isWhitespace(c)) { - if (c == 'x' && text.charAt(j - 1) == 'p') { // ends with px - text = text.trim(); - if (text.matches("\\d+px") && text.charAt(0) != '0') { //$NON-NLS-1$ - if (context.isEnabled(PX_ISSUE)) { - context.report(PX_ISSUE, item, context.getLocation(textNode), - "Avoid using \"px\" as units; use \"dp\" instead", null); - } - } - } else if (c == 'm' && text.charAt(j - 1) == 'm' || - c == 'n' && text.charAt(j - 1) == 'i') { - text = text.trim(); - String unit = text.substring(text.length() - 2); - if (text.matches("\\d+" + unit) && text.charAt(0) != '0') { //$NON-NLS-1$ - if (context.isEnabled(IN_MM_ISSUE)) { - context.report(IN_MM_ISSUE, item, context.getLocation(textNode), - String.format("Avoid using \"%1$s\" as units " - + "(it does not work accurately on all devices); " - + "use \"dp\" instead", unit), null); - } - } - } else if (c == 'p' && (text.charAt(j - 1) == 'd' - || text.charAt(j - 1) == 'i')) { // ends with dp or di - text = text.trim(); - String name = item.getAttribute(ATTR_NAME); - if ((name.equals(ATTR_TEXT_SIZE) - || name.equals("android:textSize")) //$NON-NLS-1$ - && text.matches("\\d+di?p")) { //$NON-NLS-1$ - if (context.isEnabled(DP_ISSUE)) { - context.report(DP_ISSUE, item, context.getLocation(textNode), - "Should use \"sp\" instead of \"dp\" for text sizes", null); - } - } - } else if (c == 'p' && text.charAt(j - 1) == 's') { - String name = item.getAttribute(ATTR_NAME); - if (ATTR_TEXT_SIZE.equals(name) || ATTR_LAYOUT_HEIGHT.equals(name)) { - text = text.trim(); - String unit = text.substring(text.length() - 2); - if (text.matches("\\d+" + unit)) { //$NON-NLS-1$ - if (context.isEnabled(SMALL_SP_ISSUE)) { - int size = getSize(text); - if (size > 0 && size < 12) { - context.report(SMALL_SP_ISSUE, item, - context.getLocation(textNode), String.format( - "Avoid using sizes smaller than 12sp: %1$s", - text), null); - } - } - } - } - } - break; - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RegistrationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RegistrationDetector.java deleted file mode 100644 index c4fd0a7..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RegistrationDetector.java +++ /dev/null @@ -1,255 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_APP_ACTIVITY; -import static com.android.SdkConstants.ANDROID_APP_SERVICE; -import static com.android.SdkConstants.ANDROID_CONTENT_BROADCAST_RECEIVER; -import static com.android.SdkConstants.ANDROID_CONTENT_CONTENT_PROVIDER; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PACKAGE; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_PROVIDER; -import static com.android.SdkConstants.TAG_RECEIVER; -import static com.android.SdkConstants.TAG_SERVICE; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Collection; -import java.util.EnumSet; -import java.util.Map.Entry; - -/** - * Checks for missing manifest registrations for activities, services etc - * and also makes sure that they are registered with the correct tag - */ -public class RegistrationDetector extends LayoutDetector implements ClassScanner { - /** Unregistered activities and services */ - public static final Issue ISSUE = Issue.create( - "Registered", //$NON-NLS-1$ - "Ensures that Activities, Services and Content Providers are registered in the manifest", - - "Activities, services and content providers should be registered in the " + - "`AndroidManifest.xml` file using `<activity>`, `<service>` and `<provider>` tags.\n" + - "\n" + - "If your activity is simply a parent class intended to be subclassed by other " + - "\"real\" activities, make it an abstract class.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - RegistrationDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.CLASS_FILE)).setMoreInfo( - "http://developer.android.com/guide/topics/manifest/manifest-intro.html"); //$NON-NLS-1$ - - private Multimap<String, String> mManifestRegistrations; - - /** Constructs a new {@link RegistrationDetector} */ - public RegistrationDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList(sTags); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String fqcn = getFqcn(element); - String tag = element.getTagName(); - String frameworkClass = tagToClass(tag); - if (frameworkClass != null) { - String signature = ClassContext.getInternalName(fqcn); - if (mManifestRegistrations == null) { - mManifestRegistrations = ArrayListMultimap.create(4, 8); - } - mManifestRegistrations.put(frameworkClass, signature); - if (signature.indexOf('$') != -1) { - // The internal name contains a $ which means it's an inner class. - // The conversion from fqcn to internal name is a bit ambiguous: - // "a.b.C.D" usually means "inner class D in class C in package a.b". - // However, it can (see issue 31592) also mean class D in package "a.b.C". - // Place *both* of these possibilities in the registered map, since this - // is only used to check that an activity is registered, not the other way - // (so it's okay to have entries there that do not correspond to real classes). - signature = signature.replace('$', '/'); - mManifestRegistrations.put(frameworkClass, signature); - } - } - } - - /** - * Returns the fully qualified class name for a manifest entry element that - * specifies a name attribute - * - * @param element the element - * @return the fully qualified class name - */ - @NonNull - private static String getFqcn(@NonNull Element element) { - Element root = element.getOwnerDocument().getDocumentElement(); - String pkg = root.getAttribute(ATTR_PACKAGE); - String className = element.getAttributeNS(ANDROID_URI, ATTR_NAME); - if (className.startsWith(".")) { //$NON-NLS-1$ - return pkg + className; - } else if (className.indexOf('.') == -1) { - // According to the <activity> manifest element documentation, this is not - // valid ( http://developer.android.com/guide/topics/manifest/activity-element.html ) - // but it appears in manifest files and appears to be supported by the runtime - // so handle this in code as well: - return pkg + '.' + className; - } // else: the class name is already a fully qualified class name - - return className; - } - - // ---- Implements ClassScanner ---- - - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - // Abstract classes do not need to be registered - if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) { - return; - } - String curr = classNode.name; - - int lastIndex = curr.lastIndexOf('$'); - if (lastIndex != -1 && lastIndex < curr.length() - 1) { - if (Character.isDigit(curr.charAt(lastIndex+1))) { - // Anonymous inner class, doesn't need to be registered - return; - } - } - - while (curr != null) { - for (String s : sClasses) { - if (curr.equals(s)) { - Collection<String> registered = mManifestRegistrations != null ? - mManifestRegistrations.get(curr) : null; - if (registered == null || !registered.contains(classNode.name)) { - report(context, classNode, curr); - } - - } - } - - curr = context.getDriver().getSuperClass(curr); - } - } - - private void report(ClassContext context, ClassNode classNode, String curr) { - String tag = classToTag(curr); - String className = ClassContext.createSignature(classNode.name, null, null); - - String wrongClass = null; // The framework class this class actually extends - if (mManifestRegistrations != null) { - Collection<Entry<String,String>> entries = - mManifestRegistrations.entries(); - for (Entry<String,String> entry : entries) { - if (entry.getValue().equals(classNode.name)) { - wrongClass = entry.getKey(); - break; - } - } - } - if (wrongClass != null) { - Location location = context.getLocation(classNode); - context.report( - ISSUE, - location, - String.format( - "%1$s is a <%2$s> but is registered in the manifest as a <%3$s>", - className, tag, classToTag(wrongClass)), - null); - } else if (!tag.equals(TAG_RECEIVER)) { // don't need to be registered - Location location = context.getLocation(classNode); - context.report( - ISSUE, - location, - String.format( - "The <%1$s> %2$s is not registered in the manifest", - tag, className), - null); - } - } - - /** The manifest tags we care about */ - private static final String[] sTags = new String[] { - TAG_ACTIVITY, - TAG_SERVICE, - TAG_RECEIVER, - TAG_PROVIDER, - // Keep synchronized with {@link #sClasses} - }; - - /** The corresponding framework classes that the tags in {@link #sTags} should extend */ - private static final String[] sClasses = new String[] { - ANDROID_APP_ACTIVITY, - ANDROID_APP_SERVICE, - ANDROID_CONTENT_BROADCAST_RECEIVER, - ANDROID_CONTENT_CONTENT_PROVIDER, - // Keep synchronized with {@link #sTags} - }; - - /** Looks up the corresponding framework class a given manifest tag's class should extend */ - private static String tagToClass(String tag) { - for (int i = 0, n = sTags.length; i < n; i++) { - if (sTags[i].equals(tag)) { - return sClasses[i]; - } - } - - return null; - } - - /** Looks up the tag a given framework class should be registered with */ - private static String classToTag(String className) { - for (int i = 0, n = sClasses.length; i < n; i++) { - if (sClasses[i].equals(className)) { - return sTags[i]; - } - } - - return null; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RequiredAttributeDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RequiredAttributeDetector.java deleted file mode 100644 index 861c1be..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/RequiredAttributeDetector.java +++ /dev/null @@ -1,618 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_NS_NAME_PREFIX; -import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_LAYOUT; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PARENT; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.FD_RES_LAYOUT; -import static com.android.SdkConstants.FN_RESOURCE_BASE; -import static com.android.SdkConstants.FQCN_GRID_LAYOUT_V7; -import static com.android.SdkConstants.GRID_LAYOUT; -import static com.android.SdkConstants.LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.REQUEST_FOCUS; -import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.TABLE_LAYOUT; -import static com.android.SdkConstants.TABLE_ROW; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.SdkConstants.VIEW_INCLUDE; -import static com.android.SdkConstants.VIEW_MERGE; -import static com.android.resources.ResourceFolderType.LAYOUT; -import static com.android.resources.ResourceFolderType.VALUES; -import static com.android.tools.lint.detector.api.LintUtils.getLayoutName; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import lombok.ast.AstVisitor; -import lombok.ast.Expression; -import lombok.ast.MethodInvocation; -import lombok.ast.NullLiteral; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.VariableReference; - -/** - * Ensures that layout width and height attributes are specified - */ -public class RequiredAttributeDetector extends LayoutDetector implements Detector.JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "RequiredSize", //$NON-NLS-1$ - "Ensures that the layout_width and layout_height are specified for all views", - - "All views must specify an explicit layout_width and layout_height attribute. " + - "There is a runtime check for this, so if you fail to specify a size, an exception " + - "is thrown at runtime.\n" + - "\n" + - "It's possible to specify these widths via styles as well. GridLayout, as a special " + - "case, does not require you to specify a size.", - Category.CORRECTNESS, - 4, - Severity.ERROR, - RequiredAttributeDetector.class, - EnumSet.of(Scope.JAVA_FILE, Scope.ALL_RESOURCE_FILES)); - - /** Map from each style name to parent style */ - @Nullable private Map<String, String> mStyleParents; - - /** Set of style names where the style sets the layout width */ - @Nullable private Set<String> mWidthStyles; - - /** Set of style names where the style sets the layout height */ - @Nullable private Set<String> mHeightStyles; - - /** Set of layout names for layouts that are included by an {@code <include>} tag - * where the width is set on the include */ - @Nullable private Set<String> mIncludedWidths; - - /** Set of layout names for layouts that are included by an {@code <include>} tag - * where the height is set on the include */ - @Nullable private Set<String> mIncludedHeights; - - /** Set of layout names for layouts that are included by an {@code <include>} tag - * where the width is <b>not</b> set on the include */ - @Nullable private Set<String> mNotIncludedWidths; - - /** Set of layout names for layouts that are included by an {@code <include>} tag - * where the height is <b>not</b> set on the include */ - @Nullable private Set<String> mNotIncludedHeights; - - /** Whether the width was set in a theme definition */ - private boolean mSetWidthInTheme; - - /** Whether the height was set in a theme definition */ - private boolean mSetHeightInTheme; - - /** Constructs a new {@link RequiredAttributeDetector} */ - public RequiredAttributeDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == LAYOUT || folderType == VALUES; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - // Process checks in two phases: - // Phase 1: Gather styles and includes (styles are encountered after the layouts - // so we can't do it in a single phase, and includes can be affected by includes from - // layouts we haven't seen yet) - // Phase 2: Process layouts, using gathered style and include data, and mark layouts - // not known. - // - if (context.getPhase() == 1) { - checkSizeSetInTheme(); - - context.requestRepeat(this, Scope.RESOURCE_FILE_SCOPE); - } - } - - private boolean isWidthStyle(String style) { - return isSizeStyle(style, mWidthStyles); - } - - private boolean isHeightStyle(String style) { - return isSizeStyle(style, mHeightStyles); - } - - private boolean isSizeStyle(String style, Set<String> sizeStyles) { - if (isFrameworkSizeStyle(style)) { - return true; - } - if (sizeStyles == null) { - return false; - } - return isSizeStyle(stripStylePrefix(style), sizeStyles, 0); - } - - private static boolean isFrameworkSizeStyle(String style) { - // The styles Widget.TextView.ListSeparator (and several theme variations, such as - // Widget.Holo.TextView.ListSeparator, Widget.Holo.Light.TextView.ListSeparator, etc) - // define layout_width and layout_height. - // These are exposed through the listSeparatorTextViewStyle style. - if (style.equals("?android:attr/listSeparatorTextViewStyle") //$NON-NLS-1$ - || style.equals("?android/listSeparatorTextViewStyle")) { //$NON-NLS-1$ - return true; - } - - // It's also set on Widget.QuickContactBadge and Widget.QuickContactBadgeSmall - // These are exposed via a handful of attributes with a common prefix - if (style.startsWith("?android:attr/quickContactBadgeStyle")) { //$NON-NLS-1$ - return true; - } - - // Finally, the styles are set on MediaButton and Widget.Holo.Tab (and - // Widget.Holo.Light.Tab) but these are not exposed via attributes. - - return false; - } - - private boolean isSizeStyle( - @NonNull String style, - @NonNull Set<String> sizeStyles, int depth) { - if (depth == 30) { - // Cycle between local and framework attribute style missed - // by the fact that we're stripping the distinction between framework - // and local styles here - return false; - } - - assert !style.startsWith(STYLE_RESOURCE_PREFIX) - && !style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX); - - if (sizeStyles.contains(style)) { - return true; - } - - if (mStyleParents != null) { - String parentStyle = mStyleParents.get(style); - if (parentStyle != null) { - parentStyle = stripStylePrefix(parentStyle); - if (isSizeStyle(parentStyle, sizeStyles, depth + 1)) { - return true; - } - } - } - - int index = style.lastIndexOf('.'); - if (index > 0) { - return isSizeStyle(style.substring(0, index), sizeStyles, depth + 1); - } - - return false; - } - - private void checkSizeSetInTheme() { - // Look through the styles and determine whether each style is a theme - if (mStyleParents == null) { - return; - } - - Map<String, Boolean> isTheme = Maps.newHashMap(); - for (String style : mStyleParents.keySet()) { - if (isTheme(stripStylePrefix(style), isTheme, 0)) { - mSetWidthInTheme = true; - mSetHeightInTheme = true; - break; - } - } - } - - private boolean isTheme(String style, Map<String, Boolean> isTheme, int depth) { - if (depth == 30) { - // Cycle between local and framework attribute style missed - // by the fact that we're stripping the distinction between framework - // and local styles here - return false; - } - - assert !style.startsWith(STYLE_RESOURCE_PREFIX) - && !style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX); - - Boolean known = isTheme.get(style); - if (known != null) { - return known; - } - - if (style.contains("Theme")) { //$NON-NLS-1$ - isTheme.put(style, true); - return true; - } - - if (mStyleParents != null) { - String parentStyle = mStyleParents.get(style); - if (parentStyle != null) { - parentStyle = stripStylePrefix(parentStyle); - if (isTheme(parentStyle, isTheme, depth + 1)) { - isTheme.put(style, true); - return true; - } - } - } - - int index = style.lastIndexOf('.'); - if (index > 0) { - String parentStyle = style.substring(0, index); - boolean result = isTheme(parentStyle, isTheme, depth + 1); - isTheme.put(style, result); - return result; - } - - return false; - } - - private static boolean hasLayoutVariations(File file) { - File parent = file.getParentFile(); - if (parent == null) { - return false; - } - File res = file.getParentFile(); - if (res == null) { - return false; - } - String name = file.getName(); - File[] folders = res.listFiles(); - if (folders == null) { - return false; - } - for (File folder : folders) { - if (!folder.getName().startsWith(FD_RES_LAYOUT)) { - continue; - } - if (folder.equals(parent)) { - continue; - } - File other = new File(folder, name); - if (other.exists()) { - return true; - } - } - - return false; - } - - private static String stripStylePrefix(@NonNull String style) { - if (style.startsWith(STYLE_RESOURCE_PREFIX)) { - style = style.substring(STYLE_RESOURCE_PREFIX.length()); - } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { - style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length()); - } - - return style; - } - - private static boolean isRootElement(@NonNull Node node) { - return node == node.getOwnerDocument().getDocumentElement(); - } - - // ---- Implements XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return ALL; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - ResourceFolderType folderType = context.getResourceFolderType(); - int phase = context.getPhase(); - if (phase == 1 && folderType == VALUES) { - String tag = element.getTagName(); - if (TAG_STYLE.equals(tag)) { - String parent = element.getAttribute(ATTR_PARENT); - if (parent != null && !parent.isEmpty()) { - String name = element.getAttribute(ATTR_NAME); - if (name != null && !name.isEmpty()) { - if (mStyleParents == null) { - mStyleParents = Maps.newHashMap(); - } - mStyleParents.put(name, parent); - } - } - } else if (TAG_ITEM.equals(tag) - && TAG_STYLE.equals(element.getParentNode().getNodeName())) { - String name = element.getAttribute(ATTR_NAME); - if (name.endsWith(ATTR_LAYOUT_WIDTH) && - name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_WIDTH)) { - if (mWidthStyles == null) { - mWidthStyles = Sets.newHashSet(); - } - String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME); - mWidthStyles.add(styleName); - } - if (name.endsWith(ATTR_LAYOUT_HEIGHT) && - name.equals(ANDROID_NS_NAME_PREFIX + ATTR_LAYOUT_HEIGHT)) { - if (mHeightStyles == null) { - mHeightStyles = Sets.newHashSet(); - } - String styleName = ((Element) element.getParentNode()).getAttribute(ATTR_NAME); - mHeightStyles.add(styleName); - } - } - } else if (folderType == LAYOUT) { - if (phase == 1) { - // Gather includes - if (element.getTagName().equals(VIEW_INCLUDE)) { - String layout = element.getAttribute(ATTR_LAYOUT); - if (layout != null && !layout.isEmpty()) { - recordIncludeWidth(layout, - element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH)); - recordIncludeHeight(layout, - element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT)); - } - } - } else { - assert phase == 2; // Check everything using style data and include data - boolean hasWidth = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WIDTH); - boolean hasHeight = element.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_HEIGHT); - - if (mSetWidthInTheme) { - hasWidth = true; - } - - if (mSetHeightInTheme) { - hasHeight = true; - } - - if (hasWidth && hasHeight) { - return; - } - - String tag = element.getTagName(); - if (VIEW_MERGE.equals(tag) - || VIEW_INCLUDE.equals(tag) - || REQUEST_FOCUS.equals(tag)) { - return; - } - - String parentTag = element.getParentNode() != null - ? element.getParentNode().getNodeName() : ""; - if (TABLE_LAYOUT.equals(parentTag) - || TABLE_ROW.equals(parentTag) - || GRID_LAYOUT.equals(parentTag) - || FQCN_GRID_LAYOUT_V7.equals(parentTag)) { - return; - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - boolean certain = true; - boolean isRoot = isRootElement(element); - if (isRoot || isRootElement(element.getParentNode()) - && VIEW_MERGE.equals(parentTag)) { - String name = LAYOUT_RESOURCE_PREFIX + getLayoutName(context.file); - if (!hasWidth && mIncludedWidths != null) { - hasWidth = mIncludedWidths.contains(name); - // If the layout is *also* included in a context where the width - // was not set, we're not certain; it's possible that - if (mNotIncludedWidths != null && mNotIncludedWidths.contains(name)) { - hasWidth = false; - // If we only have a single layout we know that this layout isn't - // always included with layout_width or layout_height set, but - // if there are multiple layouts, it's possible that at runtime - // we only load the size-less layout by the tag which includes - // the size - certain = !hasLayoutVariations(context.file); - } - } - if (!hasHeight && mIncludedHeights != null) { - hasHeight = mIncludedHeights.contains(name); - if (mNotIncludedHeights != null && mNotIncludedHeights.contains(name)) { - hasHeight = false; - certain = !hasLayoutVariations(context.file); - } - } - if (hasWidth && hasHeight) { - return; - } - } - - if (!hasWidth || !hasHeight) { - String style = element.getAttribute(ATTR_STYLE); - if (style != null && !style.isEmpty()) { - if (!hasWidth) { - hasWidth = isWidthStyle(style); - } - if (!hasHeight) { - hasHeight = isHeightStyle(style); - } - } - if (hasWidth && hasHeight) { - return; - } - } - - String message; - if (!(hasWidth || hasHeight)) { - if (certain) { - message = "The required layout_width and layout_height attributes " + - "are missing"; - } else { - message = "The required layout_width and layout_height attributes " + - "*may* be missing"; - } - } else { - String attribute = hasWidth ? ATTR_LAYOUT_HEIGHT : ATTR_LAYOUT_WIDTH; - if (certain) { - message = String.format("The required %1$s attribute is missing", - attribute); - } else { - message = String.format("The required %1$s attribute *may* be missing", - attribute); - } - } - context.report(ISSUE, element, context.getLocation(element), - message, null); - } - } - } - - private void recordIncludeWidth(String layout, boolean providesWidth) { - if (providesWidth) { - if (mIncludedWidths == null) { - mIncludedWidths = Sets.newHashSet(); - } - mIncludedWidths.add(layout); - } else { - if (mNotIncludedWidths == null) { - mNotIncludedWidths = Sets.newHashSet(); - } - mNotIncludedWidths.add(layout); - } - } - - private void recordIncludeHeight(String layout, boolean providesHeight) { - if (providesHeight) { - if (mIncludedHeights == null) { - mIncludedHeights = Sets.newHashSet(); - } - mIncludedHeights.add(layout); - } else { - if (mNotIncludedHeights == null) { - mNotIncludedHeights = Sets.newHashSet(); - } - mNotIncludedHeights.add(layout); - } - } - - // ---- Implements JavaScanner ---- - - @Override - @Nullable - public List<String> getApplicableMethodNames() { - return Collections.singletonList("inflate"); //$NON-NLS-1$ - } - - @Override - public void visitMethod( - @NonNull JavaContext context, - @Nullable AstVisitor visitor, - @NonNull MethodInvocation call) { - // Handle - // View#inflate(Context context, int resource, ViewGroup root) - // LayoutInflater#inflate(int resource, ViewGroup root) - // LayoutInflater#inflate(int resource, ViewGroup root, boolean attachToRoot) - StrictListAccessor<Expression, MethodInvocation> args = call.astArguments(); - - String layout = null; - int index = 0; - for (Iterator<Expression> iterator = args.iterator(); iterator.hasNext(); index++) { - Expression expression = iterator.next(); - if (expression instanceof Select) { - Select outer = (Select) expression; - Expression operand = outer.astOperand(); - if (operand instanceof Select) { - Select inner = (Select) operand; - if (inner.astOperand() instanceof VariableReference) { - VariableReference reference = (VariableReference) inner.astOperand(); - if (FN_RESOURCE_BASE.equals(reference.astIdentifier().astValue()) - // TODO: constant - && "layout".equals(inner.astIdentifier().astValue())) { - layout = LAYOUT_RESOURCE_PREFIX + outer.astIdentifier().astValue(); - break; - } - } - } - } - } - - if (layout == null) { - lombok.ast.Node method = StringFormatDetector.getParentMethod(call); - if (method != null) { - // Must track local types - index = 0; - String name = StringFormatDetector.getResourceArg(method, call, index); - if (name == null) { - index = 1; - name = StringFormatDetector.getResourceArg(method, call, index); - } - if (name != null) { - layout = LAYOUT_RESOURCE_PREFIX + name; - } - } - if (layout == null) { - // Flow analysis didn't succeed - return; - } - } - - // In all the applicable signatures, the view root argument is immediately after - // the layout resource id. - int viewRootPos = index + 1; - if (viewRootPos < args.size()) { - int i = 0; - Iterator<Expression> iterator = args.iterator(); - while (iterator.hasNext() && i < viewRootPos) { - iterator.next(); - i++; - } - if (iterator.hasNext()) { - Expression viewRoot = iterator.next(); - if (viewRoot instanceof NullLiteral) { - // Yep, this one inflates the given view with a null parent: - // Tag it as such. For now just use the include data structure since - // it has the same net effect - recordIncludeWidth(layout, true); - recordIncludeHeight(layout, true); - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ScrollViewChildDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ScrollViewChildDetector.java deleted file mode 100644 index 81bb522..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ScrollViewChildDetector.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_LAYOUT_HEIGHT; -import static com.android.SdkConstants.ATTR_LAYOUT_WIDTH; -import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; -import static com.android.SdkConstants.SCROLL_VIEW; -import static com.android.SdkConstants.VALUE_FILL_PARENT; -import static com.android.SdkConstants.VALUE_MATCH_PARENT; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -/** - * Check which looks at the children of ScrollViews and ensures that they fill/match - * the parent width instead of setting wrap_content. - */ -public class ScrollViewChildDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ScrollViewSize", //$NON-NLS-1$ - "Checks that ScrollViews use wrap_content in scrolling dimension", - // TODO add a better explanation here! - "ScrollView children must set their `layout_width` or `layout_height` attributes " + - "to `wrap_content` rather than `fill_parent` or `match_parent` in the scrolling " + - "dimension", - Category.CORRECTNESS, - 7, - Severity.WARNING, - ScrollViewChildDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link ScrollViewChildDetector} */ - public ScrollViewChildDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - SCROLL_VIEW, - HORIZONTAL_SCROLL_VIEW - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - List<Element> children = LintUtils.getChildren(element); - boolean isHorizontal = HORIZONTAL_SCROLL_VIEW.equals(element.getTagName()); - String attributeName = isHorizontal ? ATTR_LAYOUT_WIDTH : ATTR_LAYOUT_HEIGHT; - for (Element child : children) { - Attr sizeNode = child.getAttributeNodeNS(ANDROID_URI, attributeName); - if (sizeNode == null) { - return; - } - String value = sizeNode.getValue(); - if (VALUE_FILL_PARENT.equals(value) || VALUE_MATCH_PARENT.equals(value)) { - String msg = String.format("This %1$s should use android:%2$s=\"wrap_content\"", - child.getTagName(), attributeName); - context.report(ISSUE, sizeNode, context.getLocation(sizeNode), msg, - null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SdCardDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SdCardDetector.java deleted file mode 100644 index c876b96..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SdCardDetector.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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 java.io.File; -import java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.Node; -import lombok.ast.StringLiteral; - -/** - * Looks for hardcoded references to /sdcard/. - */ -public class SdCardDetector extends Detector implements Detector.JavaScanner { - /** Hardcoded /sdcard/ references */ - public static final Issue ISSUE = Issue.create( - "SdCardPath", //$NON-NLS-1$ - "Looks for hardcoded references to /sdcard", - - "Your code should not reference the `/sdcard` path directly; instead use " + - "`Environment.getExternalStorageDirectory().getPath()`.\n" + - "\n" + - "Similarly, do not reference the `/data/data/` path directly; it can vary " + - "in multi-user scenarios. Instead, use " + - "`Context.getFilesDir().getPath()`.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - SdCardDetector.class, - Scope.JAVA_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/data/data-storage.html#filesExternal"); //$NON-NLS-1$ - - /** Constructs a new {@link SdCardDetector} check */ - public SdCardDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements JavaScanner ---- - - @Override - public List<Class<? extends Node>> getApplicableNodeTypes() { - return Collections.<Class<? extends Node>>singletonList(StringLiteral.class); - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new StringChecker(context); - } - - private static class StringChecker extends ForwardingAstVisitor { - private final JavaContext mContext; - - public StringChecker(JavaContext context) { - mContext = context; - } - - @Override - public boolean visitStringLiteral(StringLiteral node) { - String s = node.astValue(); - if (s.isEmpty()) { - return false; - } - char c = s.charAt(0); - if (c != '/' && c != 'f') { - return false; - } - - if (s.startsWith("/sdcard") //$NON-NLS-1$ - || s.startsWith("/mnt/sdcard/") //$NON-NLS-1$ - || s.startsWith("/system/media/sdcard") //$NON-NLS-1$ - || s.startsWith("file://sdcard/") //$NON-NLS-1$ - || s.startsWith("file:///sdcard/")) { //$NON-NLS-1$ - String message = "Do not hardcode \"/sdcard/\"; " + - "use Environment.getExternalStorageDirectory().getPath() instead"; - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, message, s); - } else if (s.startsWith("/data/data/") //$NON-NLS-1$ - || s.startsWith("/data/user/")) { //$NON-NLS-1$ - String message = "Do not hardcode \"/data/\"; " + - "use Context.getFilesDir().getPath() instead"; - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, message, s); - } - - return false; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java deleted file mode 100644 index f1d349b..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecureRandomDetector.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.BasicInterpreter; -import org.objectweb.asm.tree.analysis.BasicValue; -import org.objectweb.asm.tree.analysis.Frame; - -import java.util.Collections; -import java.util.List; - -/** - * Checks for hardcoded seeds with random numbers. - */ -public class SecureRandomDetector extends Detector implements ClassScanner { - /** Unregistered activities and services */ - public static final Issue ISSUE = Issue.create( - "SecureRandom", //$NON-NLS-1$ - "Looks for suspicious usage of the SecureRandom class", - - "Specifying a fixed seed will cause the instance to return a predictable sequence " + - "of numbers. This may be useful for testing but it is not appropriate for secure use.", - - Category.PERFORMANCE, - 9, - Severity.WARNING, - SecureRandomDetector.class, - Scope.CLASS_FILE_SCOPE). - setMoreInfo("http://developer.android.com/reference/java/security/SecureRandom.html"); - - private static final String SET_SEED = "setSeed"; //$NON-NLS-1$ - private static final String OWNER_SECURE_RANDOM = "java/security/SecureRandom"; //$NON-NLS-1$ - private static final String OWNER_RANDOM = "java/util/Random"; //$NON-NLS-1$ - private static final String VM_SECURE_RANDOM = 'L' + OWNER_SECURE_RANDOM + ';'; - /** Method description for a method that takes a long argument (no return type specified */ - private static final String LONG_ARG = "(J)"; //$NON-NLS-1$ - - /** Constructs a new {@link SecureRandomDetector} */ - public SecureRandomDetector() { - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Collections.singletonList(SET_SEED); - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - String owner = call.owner; - String desc = call.desc; - if (owner.equals(OWNER_SECURE_RANDOM)) { - if (desc.startsWith(LONG_ARG)) { - checkValidSetSeed(context, call); - } else if (desc.startsWith("([B)")) { //$NON-NLS-1$ - // setSeed(byte[]) ... - // We could do some flow analysis here to see whether the byte array getting - // passed in appears to be fixed. - // However, people calling this constructor rather than the simpler one - // with a fixed integer are probably less likely to make that mistake... right? - } - } else if (owner.equals(OWNER_RANDOM) && desc.startsWith(LONG_ARG)) { - // Called setSeed(long) on an instanceof a Random object. Flag this if the instance - // is likely a SecureRandom. - - // Track allocations such that we know whether the type of the call - // is on a SecureRandom rather than a Random - Analyzer analyzer = new Analyzer(new BasicInterpreter() { - @Override - public BasicValue newValue(Type type) { - if (type != null && type.getDescriptor().equals(VM_SECURE_RANDOM)) { - return new BasicValue(type); - } - return super.newValue(type); - } - }); - try { - Frame[] frames = analyzer.analyze(classNode.name, method); - InsnList instructions = method.instructions; - Frame frame = frames[instructions.indexOf(call)]; - int stackSlot = frame.getStackSize(); - for (Type type : Type.getArgumentTypes(desc)) { - stackSlot -= type.getSize(); - } - BasicValue stackValue = (BasicValue) frame.getStack(stackSlot); - Type type = stackValue.getType(); - if (type != null && type.getDescriptor().equals(VM_SECURE_RANDOM)) { - checkValidSetSeed(context, call); - } - } catch (AnalyzerException e) { - context.log(e, null); - } - } else if (owner.equals(OWNER_RANDOM) && desc.startsWith(LONG_ARG)) { - // Called setSeed(long) on an instanceof a Random object. Flag this if the instance - // is likely a SecureRandom. - // TODO - } - } - - private static void checkValidSetSeed(ClassContext context, MethodInsnNode call) { - assert call.name.equals(SET_SEED); - - // Make sure the argument passed is not a literal - AbstractInsnNode prev = LintUtils.getPrevInstruction(call); - if (prev == null) { - return; - } - int opcode = prev.getOpcode(); - if (opcode == Opcodes.LCONST_0 || opcode == Opcodes.LCONST_1 || opcode == Opcodes.LDC) { - context.report(ISSUE, context.getLocation(call), - "Do not call setSeed() on a SecureRandom with a fixed seed: " + - "it is not secure. Use getSeed().", - null); - } else if (opcode == Opcodes.INVOKESTATIC) { - String methodName = ((MethodInsnNode) prev).name; - if (methodName.equals("currentTimeMillis") || methodName.equals("nanoTime")) { - context.report(ISSUE, context.getLocation(call), - "It is dangerous to seed SecureRandom with the current time because " + - "that value is more predictable to an attacker than the default seed.", - null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecurityDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecurityDetector.java deleted file mode 100644 index b6cc957..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SecurityDetector.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_EXPORTED; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PATH; -import static com.android.SdkConstants.ATTR_PATH_PATTERN; -import static com.android.SdkConstants.ATTR_PATH_PREFIX; -import static com.android.SdkConstants.ATTR_PERMISSION; -import static com.android.SdkConstants.ATTR_READ_PERMISSION; -import static com.android.SdkConstants.ATTR_WRITE_PERMISSION; -import static com.android.SdkConstants.TAG_ACTIVITY; -import static com.android.SdkConstants.TAG_APPLICATION; -import static com.android.SdkConstants.TAG_GRANT_PERMISSION; -import static com.android.SdkConstants.TAG_INTENT_FILTER; -import static com.android.SdkConstants.TAG_PATH_PERMISSION; -import static com.android.SdkConstants.TAG_PROVIDER; -import static com.android.SdkConstants.TAG_RECEIVER; -import static com.android.SdkConstants.TAG_SERVICE; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.Identifier; -import lombok.ast.MethodInvocation; -import lombok.ast.StrictListAccessor; - -/** - * Checks that exported services request a permission. - */ -public class SecurityDetector extends Detector implements Detector.XmlScanner, - Detector.JavaScanner { - - /** Exported services */ - public static final Issue EXPORTED_SERVICE = Issue.create( - "ExportedService", //$NON-NLS-1$ - "Checks for exported services that do not require permissions", - "Exported services (services which either set `exported=true` or contain " + - "an intent-filter and do not specify `exported=false`) should define a " + - "permission that an entity must have in order to launch the service " + - "or bind to it. Without this, any application can use this service.", - Category.SECURITY, - 5, - Severity.WARNING, - SecurityDetector.class, - Scope.MANIFEST_SCOPE); - - /** Exported content providers */ - public static final Issue EXPORTED_PROVIDER = Issue.create( - "ExportedContentProvider", //$NON-NLS-1$ - "Checks for exported content providers that do not require permissions", - "Content providers are exported by default and any application on the " + - "system can potentially use them to read and write data. If the content " + - "provider provides access to sensitive data, it should be protected by " + - "specifying `export=false` in the manifest or by protecting it with a " + - "permission that can be granted to other applications.", - Category.SECURITY, - 5, - Severity.WARNING, - SecurityDetector.class, - Scope.MANIFEST_SCOPE); - - /** Exported receivers */ - public static final Issue EXPORTED_RECEIVER = Issue.create( - "ExportedReceiver", //$NON-NLS-1$ - "Checks for exported receivers that do not require permissions", - "Exported receivers (receivers which either set `exported=true` or contain " + - "an intent-filter and do not specify `exported=false`) should define a " + - "permission that an entity must have in order to launch the receiver " + - "or bind to it. Without this, any application can use this receiver.", - Category.SECURITY, - 5, - Severity.WARNING, - SecurityDetector.class, - Scope.MANIFEST_SCOPE); - - /** Content provides which grant all URIs access */ - public static final Issue OPEN_PROVIDER = Issue.create( - "GrantAllUris", //$NON-NLS-1$ - "Checks for <grant-uri-permission> elements where everything is shared", - "The `<grant-uri-permission>` element allows specific paths to be shared. " + - "This detector checks for a path URL of just '/' (everything), which is " + - "probably not what you want; you should limit access to a subset.", - Category.SECURITY, - 7, - Severity.WARNING, - SecurityDetector.class, - Scope.MANIFEST_SCOPE); - - /** Using the world-writable flag */ - public static final Issue WORLD_WRITEABLE = Issue.create( - "WorldWriteableFiles", //$NON-NLS-1$ - "Checks for openFileOutput() and getSharedPreferences() calls passing " + - "MODE_WORLD_WRITEABLE", - "There are cases where it is appropriate for an application to write " + - "world writeable files, but these should be reviewed carefully to " + - "ensure that they contain no private data, and that if the file is " + - "modified by a malicious application it does not trick or compromise " + - "your application.", - Category.SECURITY, - 4, - Severity.WARNING, - SecurityDetector.class, - Scope.JAVA_FILE_SCOPE); - - - /** Using the world-readable flag */ - public static final Issue WORLD_READABLE = Issue.create( - "WorldReadableFiles", //$NON-NLS-1$ - "Checks for openFileOutput() and getSharedPreferences() calls passing " + - "MODE_WORLD_READABLE", - "There are cases where it is appropriate for an application to write " + - "world readable files, but these should be reviewed carefully to " + - "ensure that they contain no private data that is leaked to other " + - "applications.", - Category.SECURITY, - 4, - Severity.WARNING, - SecurityDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Constructs a new {@link SecurityDetector} check */ - public SecurityDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - // ---- Implements Detector.XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_SERVICE, - TAG_GRANT_PERMISSION, - TAG_PROVIDER, - TAG_ACTIVITY, - TAG_RECEIVER - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - if (tag.equals(TAG_SERVICE)) { - checkService(context, element); - } else if (tag.equals(TAG_GRANT_PERMISSION)) { - checkGrantPermission(context, element); - } else if (tag.equals(TAG_PROVIDER)) { - checkProvider(context, element); - } else if (tag.equals(TAG_RECEIVER)) { - checkReceiver(context, element); - } - } - - private static boolean getExported(Element element) { - // Used to check whether an activity, service or broadcast receiver is exported. - String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED); - if (exportValue != null && !exportValue.isEmpty()) { - return Boolean.valueOf(exportValue); - } else { - for (Element child : LintUtils.getChildren(element)) { - if (child.getTagName().equals(TAG_INTENT_FILTER)) { - return true; - } - } - } - - return false; - } - - private static boolean isUnprotectedByPermission(Element element) { - // Used to check whether an activity, service or broadcast receiver are - // protected by a permission. - String permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); - if (permission == null || permission.isEmpty()) { - Node parent = element.getParentNode(); - if (parent.getNodeType() == Node.ELEMENT_NODE - && parent.getNodeName().equals(TAG_APPLICATION)) { - Element application = (Element) parent; - permission = application.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); - return permission == null || permission.isEmpty(); - } - } - - return false; - } - - private static boolean isLauncher(Element element) { - // Checks whether an element is a launcher activity. - for (Element child : LintUtils.getChildren(element)) { - if (child.getTagName().equals(TAG_INTENT_FILTER)) { - for (Element innerChild: LintUtils.getChildren(child)) { - if (innerChild.getTagName().equals("category")) { //$NON-NLS-1$ - String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME); - return "android.intent.category.LAUNCHER".equals(categoryString); //$NON-NLS-1$ - } - } - } - } - - return false; - } - - private static boolean isStandardReceiver(Element element) { - // Checks whether a broadcast receiver receives a standard Android action - for (Element child : LintUtils.getChildren(element)) { - if (child.getTagName().equals(TAG_INTENT_FILTER)) { - for (Element innerChild : LintUtils.getChildren(child)) { - if (innerChild.getTagName().equals("action")) { //$NON-NLS-1$ - String categoryString = innerChild.getAttributeNS(ANDROID_URI, ATTR_NAME); - return categoryString.startsWith("android."); //$NON-NLS-1$ - } - } - } - } - return false; - } - - private static void checkReceiver(XmlContext context, Element element) { - if (getExported(element) && isUnprotectedByPermission(element) && - !isStandardReceiver(element)) { - // No declared permission for this exported receiver: complain - context.report(EXPORTED_RECEIVER, element, context.getLocation(element), - "Exported receiver does not require permission", null); - } - } - - private static void checkService(XmlContext context, Element element) { - if (getExported(element) && isUnprotectedByPermission(element)) { - // No declared permission for this exported service: complain - context.report(EXPORTED_SERVICE, element, context.getLocation(element), - "Exported service does not require permission", null); - } - } - - private static void checkGrantPermission(XmlContext context, Element element) { - Attr path = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH); - Attr prefix = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PREFIX); - Attr pattern = element.getAttributeNodeNS(ANDROID_URI, ATTR_PATH_PATTERN); - - String msg = "Content provider shares everything; this is potentially dangerous."; - if (path != null && path.getValue().equals("/")) { //$NON-NLS-1$ - context.report(OPEN_PROVIDER, path, context.getLocation(path), msg, null); - } - if (prefix != null && prefix.getValue().equals("/")) { //$NON-NLS-1$ - context.report(OPEN_PROVIDER, prefix, context.getLocation(prefix), msg, null); - } - if (pattern != null && (pattern.getValue().equals("/") //$NON-NLS-1$ - /* || pattern.getValue().equals(".*")*/)) { - context.report(OPEN_PROVIDER, pattern, context.getLocation(pattern), msg, null); - } - } - - private static void checkProvider(XmlContext context, Element element) { - String exportValue = element.getAttributeNS(ANDROID_URI, ATTR_EXPORTED); - // Content providers are exported by default - boolean exported = true; - if (exportValue != null && !exportValue.isEmpty()) { - exported = Boolean.valueOf(exportValue); - } - - if (exported) { - // Just check for some use of permissions. Other Lint checks can check the saneness - // of the permissions. We'll accept the permission, readPermission, or writePermission - // attributes on the provider element, or a path-permission element. - String permission = element.getAttributeNS(ANDROID_URI, ATTR_READ_PERMISSION); - if (permission == null || permission.isEmpty()) { - permission = element.getAttributeNS(ANDROID_URI, ATTR_WRITE_PERMISSION); - if (permission == null || permission.isEmpty()) { - permission = element.getAttributeNS(ANDROID_URI, ATTR_PERMISSION); - if (permission == null || permission.isEmpty()) { - // No permission attributes? Check for path-permission. - - // TODO: Add a Lint check to ensure the path-permission is good, similar to - // the grant-uri-permission check. - boolean hasPermission = false; - for (Element child : LintUtils.getChildren(element)) { - String tag = child.getTagName(); - if (tag.equals(TAG_PATH_PERMISSION)) { - hasPermission = true; - break; - } - } - - if (!hasPermission) { - context.report(EXPORTED_PROVIDER, element, - context.getLocation(element), - "Exported content providers can provide access to " + - "potentially sensitive data", - null); - } - } - } - } - } - } - - // ---- Implements Detector.JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - // These are the API calls that can accept a MODE_WORLD_READABLE/MODE_WORLD_WRITABLE - // argument. - List<String> values = new ArrayList<String>(2); - values.add("openFileOutput"); //$NON-NLS-1$ - values.add("getSharedPreferences"); //$NON-NLS-1$ - return values; - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - StrictListAccessor<Expression,MethodInvocation> args = node.astArguments(); - Iterator<Expression> iterator = args.iterator(); - while (iterator.hasNext()) { - iterator.next().accept(visitor); - } - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new IdentifierVisitor(context); - } - - private static class IdentifierVisitor extends ForwardingAstVisitor { - private final JavaContext mContext; - - public IdentifierVisitor(JavaContext context) { - super(); - mContext = context; - } - - @Override - public boolean visitIdentifier(Identifier node) { - if ("MODE_WORLD_WRITEABLE".equals(node.getDescription())) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(WORLD_WRITEABLE, node, location, - "Using MODE_WORLD_WRITEABLE when creating files can be " + - "risky, review carefully", - null); - } else if ("MODE_WORLD_READABLE".equals(node.getDescription())) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(WORLD_READABLE, node, location, - "Using MODE_WORLD_READABLE when creating files can be " + - "risky, review carefully", - null); - } - - return false; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SetJavaScriptEnabledDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SetJavaScriptEnabledDetector.java deleted file mode 100644 index 4b4ba01..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SetJavaScriptEnabledDetector.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.MethodInvocation; - -/** - * Looks for invocations of android.webkit.WebSettings.setJavaScriptEnabled. - */ -public class SetJavaScriptEnabledDetector extends Detector implements Detector.JavaScanner { - /** Invocations of setJavaScriptEnabled */ - public static final Issue ISSUE = Issue.create("SetJavaScriptEnabled", //$NON-NLS-1$ - "Looks for invocations of android.webkit.WebSettings.setJavaScriptEnabled", - - "Your code should not invoke `setJavaScriptEnabled` if you are not sure that " + - "your app really requires JavaScript support.", - - Category.SECURITY, - 6, - Severity.WARNING, - SetJavaScriptEnabledDetector.class, - Scope.JAVA_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/practices/security.html"); //$NON-NLS-1$ - - /** Constructs a new {@link SetJavaScriptEnabledDetector} check */ - public SetJavaScriptEnabledDetector() { - } - - // ---- Implements JavaScanner ---- - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - if (node.astArguments().size() == 1 - && !node.astArguments().first().toString().equals("false")) { //$NON-NLS-1$ - context.report(ISSUE, node, context.getLocation(node), - "Using setJavaScriptEnabled can introduce XSS vulnerabilities " + - "into you application, review carefully.", - null); - } - } - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("setJavaScriptEnabled"); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SharedPrefsDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SharedPrefsDetector.java deleted file mode 100644 index 76887a5..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SharedPrefsDetector.java +++ /dev/null @@ -1,234 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.io.File; -import java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.NormalTypeBody; -import lombok.ast.Return; -import lombok.ast.VariableDeclaration; -import lombok.ast.VariableDefinition; -import lombok.ast.VariableReference; - -/** - * Detector looking for SharedPreferences.edit() calls without a corresponding - * commit() or apply() call - */ -public class SharedPrefsDetector extends Detector implements Detector.JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "CommitPrefEdits", //$NON-NLS-1$ - "Looks for code editing a SharedPreference but forgetting to call commit() on it", - - "After calling `edit()` on a `SharedPreference`, you must call `commit()` " + - "or `apply()` on the editor to save the results.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - SharedPrefsDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Constructs a new {@link SharedPrefsDetector} check */ - public SharedPrefsDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - - // ---- Implements JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("edit"); //$NON-NLS-1$ - } - - @Nullable - private static NormalTypeBody findSurroundingTypeBody(Node scope) { - while (scope != null) { - Class<? extends Node> type = scope.getClass(); - // The Lombok AST uses a flat hierarchy of node type implementation classes - // so no need to do instanceof stuff here. - if (type == NormalTypeBody.class) { - return (NormalTypeBody) scope; - } - - scope = scope.getParent(); - } - - return null; - } - - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - assert node.astName().astValue().equals("edit"); - Expression operand = node.astOperand(); - if (operand == null) { - return; - } - - // Looking for the specific pattern where you assign the edit() result - // to a local variable; this means we won't recognize some other usages - // of the API (e.g. assigning it to a previously declared variable) but - // is needed until we have type attribution in the AST itself. - Node parent = node.getParent(); - VariableDefinition definition = getLhs(parent); - boolean allowCommitBeforeTarget; - if (definition == null) { - if (operand instanceof VariableReference) { - NormalTypeBody body = findSurroundingTypeBody(parent); - if (body == null) { - return; - } - String variableName = ((VariableReference) operand).astIdentifier().astValue(); - String type = getFieldType(body, variableName); - if (type == null || !type.equals("SharedPreferences")) { //$NON-NLS-1$ - return; - } - allowCommitBeforeTarget = true; - } else { - return; - } - } else { - String type = definition.astTypeReference().toString(); - if (!type.endsWith("SharedPreferences.Editor")) { //$NON-NLS-1$ - if (!type.equals("Editor") || //$NON-NLS-1$ - !LintUtils.isImported(context.compilationUnit, - "android.content.SharedPreferences.Editor")) { //$NON-NLS-1$ - return; - } - } - allowCommitBeforeTarget = false; - } - - Node method = JavaContext.findSurroundingMethod(parent); - if (method == null) { - return; - } - - CommitFinder finder = new CommitFinder(node, allowCommitBeforeTarget); - method.accept(finder); - if (!finder.isCommitCalled()) { - context.report(ISSUE, method, context.getLocation(node), - "SharedPreferences.edit() without a corresponding commit() or apply() call", - null); - } - } - - @Nullable - private static String getFieldType(@NonNull NormalTypeBody cls, @NonNull String name) { - List<Node> children = cls.getChildren(); - for (Node child : children) { - if (child.getClass() == VariableDeclaration.class) { - VariableDeclaration declaration = (VariableDeclaration) child; - VariableDefinition definition = declaration.astDefinition(); - return definition.astTypeReference().toString(); - } - } - - return null; - } - - @Nullable - private static VariableDefinition getLhs(@NonNull Node node) { - while (node != null) { - Class<? extends Node> type = node.getClass(); - // The Lombok AST uses a flat hierarchy of node type implementation classes - // so no need to do instanceof stuff here. - if (type == MethodDeclaration.class || type == ConstructorDeclaration.class) { - return null; - } - if (type == VariableDefinition.class) { - return (VariableDefinition) node; - } - - node = node.getParent(); - } - - return null; - } - - private static class CommitFinder extends ForwardingAstVisitor { - /** The target edit call */ - private final MethodInvocation mTarget; - /** whether it allows the commit call to be seen before the target node */ - private final boolean mAllowCommitBeforeTarget; - /** Whether we've found one of the commit/cancel methods */ - private boolean mFound; - /** Whether we've seen the target edit node yet */ - private boolean mSeenTarget; - - private CommitFinder(MethodInvocation target, boolean allowCommitBeforeTarget) { - mTarget = target; - mAllowCommitBeforeTarget = allowCommitBeforeTarget; - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (node == mTarget) { - mSeenTarget = true; - } else if (mAllowCommitBeforeTarget || mSeenTarget || node.astOperand() == mTarget) { - String name = node.astName().astValue(); - if ("commit".equals(name) || "apply".equals(name)) { //$NON-NLS-1$ //$NON-NLS-2$ - // TODO: Do more flow analysis to see whether we're really calling commit/apply - // on the right type of object? - mFound = true; - } - } - - return super.visitMethodInvocation(node); - } - - @Override - public boolean visitReturn(Return node) { - if (node.astValue() == mTarget) { - // If you just do "return editor.commit() don't warn - mFound = true; - } - return super.visitReturn(node); - } - - boolean isCommitCalled() { - return mFound; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StateListDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StateListDetector.java deleted file mode 100644 index c0dea30..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StateListDetector.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -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.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Checks for unreachable states in an Android state list definition - */ -public class StateListDetector extends ResourceXmlDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "StateListReachable", //$NON-NLS-1$ - "Looks for unreachable states in a <selector>", - "In a selector, only the last child in the state list should omit a " + - "state qualifier. If not, all subsequent items in the list will be ignored " + - "since the given item will match all.", - Category.CORRECTNESS, - 5, - Severity.WARNING, - StateListDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - private static final String STATE_PREFIX = "state_"; //$NON-NLS-1$ - - /** Constructs a new {@link StateListDetector} */ - public StateListDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.DRAWABLE; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - // TODO: Look for views that don't specify - // Display the error token somewhere so it can be suppressed - // Emit warning at the end "run with --help to learn how to suppress types of errors/checks"; - // ("...and this message.") - - Element root = document.getDocumentElement(); - if (root != null && root.getTagName().equals("selector")) { //$NON-NLS-1$ - List<Element> children = LintUtils.getChildren(root); - Map<Element, Set<String>> states = - new HashMap<Element, Set<String>>(children.size()); - - for (int i = 0; i < children.size(); i++) { - Element child = children.get(i); - NamedNodeMap attributes = child.getAttributes(); - Set<String> stateNames = new HashSet<String>(attributes.getLength()); - states.put(child, stateNames); - - for (int j = 0; j < attributes.getLength(); j++) { - Attr attribute = (Attr) attributes.item(j); - String name = attribute.getLocalName(); - if (name == null) { - continue; - } - if (name.startsWith(STATE_PREFIX)) { - stateNames.add(name + '=' + attribute.getValue()); - } else { - String namespaceUri = attribute.getNamespaceURI(); - if (namespaceUri != null && !namespaceUri.isEmpty() && - !ANDROID_URI.equals(namespaceUri)) { - // There is a custom attribute on this item. - // This could be a state, see - // http://code.google.com/p/android/issues/detail?id=22339 - // so don't flag this one. - stateNames.add(attribute.getName() + '=' + attribute.getValue()); - } - } - } - } - - // See if for each state, any subsequent state fully contains all the same - // state requirements - - for (int i = 0; i < children.size() - 1; i++) { - Element prev = children.get(i); - Set<String> prevStates = states.get(prev); - assert prevStates != null : prev; - for (int j = i + 1; j < children.size(); j++) { - Element current = children.get(j); - Set<String> currentStates = states.get(current); - assert currentStates != null : current; - if (currentStates.containsAll(prevStates)) { - Location location = context.getLocation(current); - Location secondary = context.getLocation(prev); - secondary.setMessage("Earlier item which masks item"); - location.setSecondary(secondary); - context.report(ISSUE, current, location, String.format( - "This item is unreachable because a previous item (item #%1$d) is a more general match than this one", - i + 1), null); - // Don't keep reporting errors for all the remaining cases in this file - return; - } - } - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StringFormatDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StringFormatDetector.java deleted file mode 100644 index 033996e..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StringFormatDetector.java +++ /dev/null @@ -1,1299 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.FORMAT_METHOD; -import static com.android.SdkConstants.GET_STRING_METHOD; -import static com.android.SdkConstants.TAG_STRING; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.IJavaParser; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -import com.android.tools.lint.detector.api.Position; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import com.android.utils.Pair; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import lombok.ast.AstVisitor; -import lombok.ast.CharLiteral; -import lombok.ast.ConstructorDeclaration; -import lombok.ast.ConstructorInvocation; -import lombok.ast.Expression; -import lombok.ast.FloatingPointLiteral; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.IntegralLiteral; -import lombok.ast.MethodDeclaration; -import lombok.ast.MethodInvocation; -import lombok.ast.NullLiteral; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; -import lombok.ast.StringLiteral; -import lombok.ast.VariableDefinitionEntry; -import lombok.ast.VariableReference; - -/** - * Check which looks for problems with formatting strings such as inconsistencies between - * translations or between string declaration and string usage in Java. - * <p> - * TODO: Handle Resources.getQuantityString as well - */ -public class StringFormatDetector extends ResourceXmlDetector implements Detector.JavaScanner { - /** Whether formatting strings are invalid */ - public static final Issue INVALID = Issue.create( - "StringFormatInvalid", //$NON-NLS-1$ - "Checks that format strings are valid", - - "If a string contains a '%' character, then the string may be a formatting string " + - "which will be passed to `String.format` from Java code to replace each '%' " + - "occurrence with specific values.\n" + - "\n" + - "This lint warning checks for two related problems:\n" + - "(1) Formatting strings that are invalid, meaning that `String.format` will throw " + - "exceptions at runtime when attempting to use the format string.\n" + - "(2) Strings containing '%' that are not formatting strings getting passed to " + - "a `String.format` call. In this case the '%' will need to be escaped as '%%'.\n" + - "\n" + - "NOTE: Not all Strings which look like formatting strings are intended for " + - "use by `String.format`; for example, they may contain date formats intended " + - "for `android.text.format.Time#format()`. Lint cannot always figure out that " + - "a String is a date format, so you may get false warnings in those scenarios. " + - "See the suppress help topic for information on how to suppress errors in " + - "that case.", - - Category.MESSAGES, - 9, - Severity.ERROR, - StringFormatDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Whether formatting argument types are consistent across translations */ - public static final Issue ARG_COUNT = Issue.create( - "StringFormatCount", //$NON-NLS-1$ - "Ensures that all format strings are used and that the same number is defined " - + "across translations", - - "When a formatted string takes arguments, it usually needs to reference the " + - "same arguments in all translations. There are cases where this is not the case, " + - "so this issue is a warning rather than an error by default. However, this usually " + - "happens when a language is not translated or updated correctly.", - Category.MESSAGES, - 5, - Severity.WARNING, - StringFormatDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Whether the string format supplied in a call to String.format matches the format string */ - public static final Issue ARG_TYPES = Issue.create( - "StringFormatMatches", //$NON-NLS-1$ - "Ensures that the format used in <string> definitions is compatible with the " - + "String.format call", - - "This lint check ensures the following:\n" + - "(1) If there are multiple translations of the format string, then all translations " + - "use the same type for the same numbered arguments\n" + - "(2) The usage of the format string in Java is consistent with the format string, " + - "meaning that the parameter types passed to String.format matches those in the " + - "format string.", - Category.MESSAGES, - 9, - Severity.ERROR, - StringFormatDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.JAVA_FILE)); - - /** - * Map from a format string name to a list of declaration file and actual - * formatting string content. We're using a list since a format string can be - * defined multiple times, usually for different translations. - */ - private Map<String, List<Pair<Handle, String>>> mFormatStrings; - - /** - * Map of strings that contain percents that aren't formatting strings; these - * should not be passed to String.format. - */ - private final Map<String, Handle> mNotFormatStrings = new HashMap<String, Handle>(); - - /** - * Set of strings that have an unknown format such as date formatting; we should not - * flag these as invalid when used from a String#format call - */ - private Set<String> mIgnoreStrings; - - /** Constructs a new {@link StringFormatDetector} check */ - public StringFormatDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - if (LintUtils.endsWith(file.getName(), DOT_JAVA)) { - return mFormatStrings != null; - } - - return super.appliesTo(context, file); - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_STRING); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - NodeList childNodes = element.getChildNodes(); - if (childNodes.getLength() > 0) { - if (childNodes.getLength() == 1) { - Node child = childNodes.item(0); - if (child.getNodeType() == Node.TEXT_NODE) { - checkTextNode(context, element, strip(child.getNodeValue())); - } - } else { - // Concatenate children and build up a plain string. - // This is needed to handle xliff localization documents, - // but this needs more work so ignore compound XML documents as - // string values for now: - //StringBuilder sb = new StringBuilder(); - //addText(sb, element); - //if (sb.length() > 0) { - // checkTextNode(context, element, sb.toString()); - //} - } - } - } - - //private static void addText(StringBuilder sb, Node node) { - // if (node.getNodeType() == Node.TEXT_NODE) { - // sb.append(strip(node.getNodeValue().trim())); - // } else { - // NodeList childNodes = node.getChildNodes(); - // for (int i = 0, n = childNodes.getLength(); i < n; i++) { - // addText(sb, childNodes.item(i)); - // } - // } - //} - - private static String strip(String s) { - if (s.length() < 2) { - return s; - } - char first = s.charAt(0); - char last = s.charAt(s.length() - 1); - if (first == last && (first == '\'' || first == '"')) { - return s.substring(1, s.length() - 1); - } - - return s; - } - - private void checkTextNode(XmlContext context, Element element, String text) { - String name = null; - boolean found = false; - - // Look at the String and see if it's a format string (contains - // positional %'s) - for (int j = 0, m = text.length(); j < m; j++) { - char c = text.charAt(j); - if (c == '\\') { - j++; - } - if (c == '%') { - if (name == null) { - name = element.getAttribute(ATTR_NAME); - } - - // Also make sure this String isn't an unformatted String - String formatted = element.getAttribute("formatted"); //$NON-NLS-1$ - if (!formatted.isEmpty() && !Boolean.parseBoolean(formatted)) { - if (!mNotFormatStrings.containsKey(name)) { - Handle handle = context.parser.createLocationHandle(context, element); - handle.setClientData(element); - mNotFormatStrings.put(name, handle); - } - return; - } - - // See if it's not a format string, e.g. "Battery charge is 100%!". - // If so we want to record this name in a special list such that we can - // make sure you don't attempt to reference this string from a String.format - // call. - Matcher matcher = FORMAT.matcher(text); - if (!matcher.find(j)) { - if (!mNotFormatStrings.containsKey(name)) { - Handle handle = context.parser.createLocationHandle(context, element); - handle.setClientData(element); - mNotFormatStrings.put(name, handle); - } - return; - } - - String conversion = matcher.group(6); - int conversionClass = getConversionClass(conversion.charAt(0)); - if (conversionClass == CONVERSION_CLASS_UNKNOWN || matcher.group(5) != null) { - if (mIgnoreStrings == null) { - mIgnoreStrings = new HashSet<String>(); - } - mIgnoreStrings.add(name); - - // Don't process any other strings here; some of them could - // accidentally look like a string, e.g. "%H" is a hash code conversion - // in String.format (and hour in Time formatting). - return; - } - - found = true; - j++; // Ensure that when we process a "%%" we don't separately check the second % - } - } - - if (found && name != null) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - // Record it for analysis when seen in Java code - if (mFormatStrings == null) { - mFormatStrings = new HashMap<String, List<Pair<Handle,String>>>(); - } - - List<Pair<Handle, String>> list = mFormatStrings.get(name); - if (list == null) { - list = new ArrayList<Pair<Handle, String>>(); - mFormatStrings.put(name, list); - } - Handle handle = context.parser.createLocationHandle(context, element); - handle.setClientData(element); - list.add(Pair.of(handle, text)); - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mFormatStrings != null) { - boolean checkCount = context.isEnabled(ARG_COUNT); - boolean checkValid = context.isEnabled(INVALID); - boolean checkTypes = context.isEnabled(ARG_TYPES); - - // Ensure that all the format strings are consistent with respect to each other; - // e.g. they all have the same number of arguments, they all use all the - // arguments, and they all use the same types for all the numbered arguments - for (Map.Entry<String, List<Pair<Handle, String>>> entry : mFormatStrings.entrySet()) { - String name = entry.getKey(); - List<Pair<Handle, String>> list = entry.getValue(); - - // Check argument counts - if (checkCount) { - checkArity(context, name, list); - } - - // Check argument types (and also make sure that the formatting strings are valid) - if (checkValid || checkTypes) { - checkTypes(context, checkValid, checkTypes, name, list); - } - } - } - } - - private static void checkTypes(Context context, boolean checkValid, - boolean checkTypes, String name, List<Pair<Handle, String>> list) { - Map<Integer, String> types = new HashMap<Integer, String>(); - Map<Integer, Handle> typeDefinition = new HashMap<Integer, Handle>(); - for (Pair<Handle, String> pair : list) { - Handle handle = pair.getFirst(); - String formatString = pair.getSecond(); - - //boolean warned = false; - Matcher matcher = FORMAT.matcher(formatString); - int index = 0; - int prevIndex = 0; - int nextNumber = 1; - while (true) { - if (matcher.find(index)) { - int matchStart = matcher.start(); - // Make sure this is not an escaped '%' - for (; prevIndex < matchStart; prevIndex++) { - char c = formatString.charAt(prevIndex); - if (c == '\\') { - prevIndex++; - } - } - if (prevIndex > matchStart) { - // We're in an escape, ignore this result - index = prevIndex; - continue; - } - - index = matcher.end(); // Ensure loop proceeds - String str = formatString.substring(matchStart, matcher.end()); - if (str.equals("%%")) { //$NON-NLS-1$ - // Just an escaped % - continue; - } - - if (checkValid) { - // Make sure it's a valid format string - if (str.length() > 2 && str.charAt(str.length() - 2) == ' ') { - char last = str.charAt(str.length() - 1); - // If you forget to include the conversion character, e.g. - // "Weight=%1$ g" instead of "Weight=%1$d g", then - // you're going to end up with a format string interpreted as - // "%1$ g". This means that the space character is interpreted - // as a flag character, but it can only be a flag character - // when used in conjunction with the numeric conversion - // formats (d, o, x, X). If that's not the case, make a - // dedicated error message - if (last != 'd' && last != 'o' && last != 'x' && last != 'X') { - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(INVALID, - (Node) clientData)) { - return; - } - } - - Location location = handle.resolve(); - String message = String.format( - "Incorrect formatting string %1$s; missing conversion " + - "character in '%2$s' ?", name, str); - context.report(INVALID, location, message, null); - //warned = true; - continue; - } - } - } - - if (!checkTypes) { - continue; - } - - // Shouldn't throw a number format exception since we've already - // matched the pattern in the regexp - int number; - String numberString = matcher.group(1); - if (numberString != null) { - // Strip off trailing $ - numberString = numberString.substring(0, numberString.length() - 1); - number = Integer.parseInt(numberString); - nextNumber = number + 1; - } else { - number = nextNumber++; - } - String format = matcher.group(6); - String currentFormat = types.get(number); - if (currentFormat == null) { - types.put(number, format); - typeDefinition.put(number, handle); - } else if (!currentFormat.equals(format) - && isIncompatible(currentFormat.charAt(0), format.charAt(0))) { - - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ARG_TYPES, (Node) clientData)) { - return; - } - } - - Location location = handle.resolve(); - // Attempt to limit the location range to just the formatting - // string in question - location = refineLocation(context, location, formatString, - matcher.start(), matcher.end()); - Location otherLocation = typeDefinition.get(number).resolve(); - otherLocation.setMessage("Conflicting argument type here"); - location.setSecondary(otherLocation); - File f = otherLocation.getFile(); - String message = String.format( - "Inconsistent formatting types for argument #%1$d in " + - "format string %2$s ('%3$s'): Found both '%4$s' and '%5$s' " + - "(in %6$s)", - number, name, - str, - currentFormat, format, - f.getParentFile().getName() + File.separator + f.getName()); - //warned = true; - context.report(ARG_TYPES, location, message, null); - break; - } - } else { - break; - } - } - - // Check that the format string is valid by actually attempting to instantiate - // it. We only do this if we haven't already complained about this string - // for other reasons. - /* Check disabled for now: it had many false reports due to conversion - * errors (which is expected since we just pass in strings), but once those - * are eliminated there aren't really any other valid error messages returned - * (for example, calling the formatter with bogus formatting flags always just - * returns a "conversion" error. It looks like we'd need to actually pass compatible - * arguments to trigger other types of formatting errors such as precision errors. - if (!warned && checkValid) { - try { - formatter.format(formatString, "", "", "", "", "", "", "", - "", "", "", "", "", "", ""); - - } catch (IllegalFormatException t) { // TODO: UnknownFormatConversionException - if (!t.getLocalizedMessage().contains(" != ") - && !t.getLocalizedMessage().contains("Conversion")) { - Location location = handle.resolve(); - context.report(INVALID, location, - String.format("Wrong format for %1$s: %2$s", - name, t.getLocalizedMessage()), null); - } - } - } - */ - } - } - - /** - * Returns true if two String.format conversions are "incompatible" (meaning - * that using these two for the same argument across different translations - * is more likely an error than intentional. Some conversions are - * incompatible, e.g. "d" and "s" where one is a number and string, whereas - * others may work (e.g. float versus integer) but are probably not - * intentional. - */ - private static boolean isIncompatible(char conversion1, char conversion2) { - int class1 = getConversionClass(conversion1); - int class2 = getConversionClass(conversion2); - return class1 != class2 - && class1 != CONVERSION_CLASS_UNKNOWN - && class2 != CONVERSION_CLASS_UNKNOWN; - } - - private static final int CONVERSION_CLASS_UNKNOWN = 0; - private static final int CONVERSION_CLASS_STRING = 1; - private static final int CONVERSION_CLASS_CHARACTER = 2; - private static final int CONVERSION_CLASS_INTEGER = 3; - private static final int CONVERSION_CLASS_FLOAT = 4; - private static final int CONVERSION_CLASS_BOOLEAN = 5; - private static final int CONVERSION_CLASS_HASHCODE = 6; - private static final int CONVERSION_CLASS_PERCENT = 7; - private static final int CONVERSION_CLASS_NEWLINE = 8; - private static final int CONVERSION_CLASS_DATETIME = 9; - - private static int getConversionClass(char conversion) { - // See http://developer.android.com/reference/java/util/Formatter.html - switch (conversion) { - case 't': // Time/date conversion - case 'T': - return CONVERSION_CLASS_DATETIME; - case 's': // string - case 'S': // Uppercase string - return CONVERSION_CLASS_STRING; - case 'c': // character - case 'C': // Uppercase character - return CONVERSION_CLASS_CHARACTER; - case 'd': // decimal - case 'o': // octal - case 'x': // hex - case 'X': - return CONVERSION_CLASS_INTEGER; - case 'f': // decimal float - case 'e': // exponential float - case 'E': - case 'g': // decimal or exponential depending on size - case 'G': - case 'a': // hex float - case 'A': - return CONVERSION_CLASS_FLOAT; - case 'b': // boolean - case 'B': - return CONVERSION_CLASS_BOOLEAN; - case 'h': // boolean - case 'H': - return CONVERSION_CLASS_HASHCODE; - case '%': // literal - return CONVERSION_CLASS_PERCENT; - case 'n': // literal - return CONVERSION_CLASS_NEWLINE; - } - - return CONVERSION_CLASS_UNKNOWN; - } - - private static Location refineLocation(Context context, Location location, - String formatString, int substringStart, int substringEnd) { - Position startLocation = location.getStart(); - Position endLocation = location.getStart(); - if (startLocation != null && endLocation != null) { - int startOffset = startLocation.getOffset(); - int endOffset = endLocation.getOffset(); - if (startOffset >= 0) { - String contents = context.getClient().readFile(location.getFile()); - if (contents != null - && endOffset <= contents.length() && startOffset < endOffset) { - int formatOffset = contents.indexOf(formatString, startOffset); - if (formatOffset != -1 && formatOffset <= endOffset) { - return Location.create(context.file, contents, - formatOffset + substringStart, formatOffset + substringEnd); - } - } - } - } - - return location; - } - - /** - * Check that the number of arguments in the format string is consistent - * across translations, and that all arguments are used - */ - private static void checkArity(Context context, String name, List<Pair<Handle, String>> list) { - // Check to make sure that the argument counts and types are consistent - int prevCount = -1; - for (Pair<Handle, String> pair : list) { - Set<Integer> indices = new HashSet<Integer>(); - int count = getFormatArgumentCount(pair.getSecond(), indices); - Handle handle = pair.getFirst(); - if (prevCount != -1 && prevCount != count) { - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ARG_COUNT, (Node) clientData)) { - return; - } - } - Location location = handle.resolve(); - Location secondary = list.get(0).getFirst().resolve(); - secondary.setMessage("Conflicting number of arguments here"); - location.setSecondary(secondary); - String message = String.format( - "Inconsistent number of arguments in formatting string %1$s; " + - "found both %2$d and %3$d", name, prevCount, count); - context.report(ARG_COUNT, location, message, null); - break; - } - - for (int i = 1; i <= count; i++) { - if (!indices.contains(i)) { - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(ARG_COUNT, (Node) clientData)) { - return; - } - } - - Set<Integer> all = new HashSet<Integer>(); - for (int j = 1; j < count; j++) { - all.add(j); - } - all.removeAll(indices); - List<Integer> sorted = new ArrayList<Integer>(all); - Collections.sort(sorted); - Location location = handle.resolve(); - String message = String.format( - "Formatting string '%1$s' is not referencing numbered arguments %2$s", - name, sorted); - context.report(ARG_COUNT, location, message, null); - break; - } - } - - prevCount = count; - } - } - - // See java.util.Formatter docs - private static final Pattern FORMAT = Pattern.compile( - // Generic format: - // %[argument_index$][flags][width][.precision]conversion - // - "%" + //$NON-NLS-1$ - // Argument Index - "(\\d+\\$)?" + //$NON-NLS-1$ - // Flags - "([-+#, 0(\\<]*)?" + //$NON-NLS-1$ - // Width - "(\\d+)?" + //$NON-NLS-1$ - // Precision - "(\\.\\d+)?" + //$NON-NLS-1$ - // Conversion. These are all a single character, except date/time conversions - // which take a prefix of t/T: - "([tT])?" + //$NON-NLS-1$ - // The current set of conversion characters are - // b,h,s,c,d,o,x,e,f,g,a,t (as well as all those as upper-case characters), plus - // n for newlines and % as a literal %. And then there are all the time/date - // characters: HIKLm etc. Just match on all characters here since there should - // be at least one. - "([a-zA-Z%])"); //$NON-NLS-1$ - - /** Given a format string returns the format type of the given argument */ - @VisibleForTesting - static String getFormatArgumentType(String s, int argument) { - Matcher matcher = FORMAT.matcher(s); - int index = 0; - int prevIndex = 0; - int nextNumber = 1; - while (true) { - if (matcher.find(index)) { - int matchStart = matcher.start(); - // Make sure this is not an escaped '%' - for (; prevIndex < matchStart; prevIndex++) { - char c = s.charAt(prevIndex); - if (c == '\\') { - prevIndex++; - } - } - if (prevIndex > matchStart) { - // We're in an escape, ignore this result - index = prevIndex; - continue; - } - - // Shouldn't throw a number format exception since we've already - // matched the pattern in the regexp - int number; - String numberString = matcher.group(1); - if (numberString != null) { - // Strip off trailing $ - numberString = numberString.substring(0, numberString.length() - 1); - number = Integer.parseInt(numberString); - nextNumber = number + 1; - } else { - number = nextNumber++; - } - - if (number == argument) { - return matcher.group(6); - } - index = matcher.end(); - } else { - break; - } - } - - return null; - } - - /** - * Given a format string returns the number of required arguments. If the - * {@code seenArguments} parameter is not null, put the indices of any - * observed arguments into it. - */ - @VisibleForTesting - static int getFormatArgumentCount(String s, Set<Integer> seenArguments) { - Matcher matcher = FORMAT.matcher(s); - int index = 0; - int prevIndex = 0; - int nextNumber = 1; - int max = 0; - while (true) { - if (matcher.find(index)) { - if ("%".equals(matcher.group(6))) { //$NON-NLS-1$ - index = matcher.end(); - continue; - } - int matchStart = matcher.start(); - // Make sure this is not an escaped '%' - for (; prevIndex < matchStart; prevIndex++) { - char c = s.charAt(prevIndex); - if (c == '\\') { - prevIndex++; - } - } - if (prevIndex > matchStart) { - // We're in an escape, ignore this result - index = prevIndex; - continue; - } - - // Shouldn't throw a number format exception since we've already - // matched the pattern in the regexp - int number; - String numberString = matcher.group(1); - if (numberString != null) { - // Strip off trailing $ - numberString = numberString.substring(0, numberString.length() - 1); - number = Integer.parseInt(numberString); - nextNumber = number + 1; - } else { - number = nextNumber++; - } - - if (number > max) { - max = number; - } - if (seenArguments != null) { - seenArguments.add(number); - } - - index = matcher.end(); - } else { - break; - } - } - - return max; - } - - /** - * Determines whether the given {@link String#format(String, Object...)} - * formatting string is "locale dependent", meaning that its output depends - * on the locale. This is the case if it for example references decimal - * numbers of dates and times. - * - * @param format the format string - * @return true if the format is locale sensitive, false otherwise - */ - public static boolean isLocaleSpecific(@NonNull String format) { - if (format.indexOf('%') == -1) { - return false; - } - - String s = format; - Matcher matcher = FORMAT.matcher(s); - int index = 0; - int prevIndex = 0; - while (true) { - if (matcher.find(index)) { - int matchStart = matcher.start(); - // Make sure this is not an escaped '%' - for (; prevIndex < matchStart; prevIndex++) { - char c = s.charAt(prevIndex); - if (c == '\\') { - prevIndex++; - } - } - if (prevIndex > matchStart) { - // We're in an escape, ignore this result - index = prevIndex; - continue; - } - - String type = matcher.group(6); - if (!type.isEmpty()) { - char t = type.charAt(0); - - // The following formatting characters are locale sensitive: - switch (t) { - case 'd': // decimal integer - case 'e': // scientific - case 'E': - case 'f': // decimal float - case 'g': // general - case 'G': - case 't': // date/time - case 'T': - return true; - } - } - index = matcher.end(); - } else { - break; - } - } - - return false; - } - - @Override - public List<String> getApplicableMethodNames() { - return Arrays.asList(FORMAT_METHOD, GET_STRING_METHOD); - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - if (mFormatStrings == null) { - return; - } - - String methodName = node.astName().getDescription(); - if (methodName.equals(FORMAT_METHOD)) { - // String.format(getResources().getString(R.string.foo), arg1, arg2, ...) - // Check that the arguments in R.string.foo match arg1, arg2, ... - if (node.astOperand() instanceof VariableReference) { - VariableReference ref = (VariableReference) node.astOperand(); - if ("String".equals(ref.astIdentifier().astValue())) { //$NON-NLS-1$ - // Found a String.format call - // Look inside to see if we can find an R string - // Find surrounding method - checkFormatCall(context, node); - } - } - } else { - // getResources().getString(R.string.foo, arg1, arg2, ...) - // Check that the arguments in R.string.foo match arg1, arg2, ... - if (node.astArguments().size() > 1 && node.astOperand() != null ) { - checkFormatCall(context, node); - } - } - } - - private void checkFormatCall(JavaContext context, MethodInvocation node) { - lombok.ast.Node current = getParentMethod(node); - if (current != null) { - checkStringFormatCall(context, current, node); - } - } - - /** - * Check the given String.format call (with the given arguments) to see if - * the string format is being used correctly - * - * @param context the context to report errors to - * @param method the method containing the {@link String#format} call - * @param call the AST node for the {@link String#format} - */ - private void checkStringFormatCall( - JavaContext context, - lombok.ast.Node method, - MethodInvocation call) { - - StrictListAccessor<Expression, MethodInvocation> args = call.astArguments(); - if (args.isEmpty()) { - return; - } - - StringTracker tracker = new StringTracker(method, call, 0); - method.accept(tracker); - String name = tracker.getFormatStringName(); - if (name == null) { - return; - } - - if (mIgnoreStrings != null && mIgnoreStrings.contains(name)) { - return; - } - - if (mNotFormatStrings.containsKey(name)) { - Handle handle = mNotFormatStrings.get(name); - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(INVALID, (Node) clientData)) { - return; - } - } - Location location = handle.resolve(); - String message = String.format( - "Format string '%1$s' is not a valid format string so it should not be " + - "passed to String.format", - name); - context.report(INVALID, call, location, message, null); - return; - } - - // TODO: Need type information in the AST - Iterator<Expression> argIterator = args.iterator(); - Expression first = argIterator.next(); - Expression second = argIterator.hasNext() ? argIterator.next() : null; - String firstName = first.toString(); - boolean specifiesLocale = firstName.startsWith("Locale.") //$NON-NLS-1$ - || firstName.contains("locale") //$NON-NLS-1$ - || firstName.equals("null") //$NON-NLS-1$ - || second != null && second.toString().contains("getString"); //$NON-NLS-1$ - - List<Pair<Handle, String>> list = mFormatStrings.get(name); - if (list != null) { - for (Pair<Handle, String> pair : list) { - String s = pair.getSecond(); - int count = getFormatArgumentCount(s, null); - Handle handle = pair.getFirst(); - if (count != args.size() - 1 - (specifiesLocale ? 1 : 0)) { - Location location = context.parser.getLocation(context, call); - Location secondary = handle.resolve(); - secondary.setMessage(String.format("This definition requires %1$d arguments", - count)); - location.setSecondary(secondary); - String message = String.format( - "Wrong argument count, format string %1$s requires %2$d but format " + - "call supplies %3$d", - name, count, args.size() - 1); - context.report(ARG_TYPES, method, location, message, null); - } else { - for (int i = 1; i <= count; i++) { - int argumentIndex = i + (specifiesLocale ? 1 : 0); - Class<?> type = tracker.getArgumentType(argumentIndex); - if (type != null) { - boolean valid = true; - String formatType = getFormatArgumentType(s, i); - char last = formatType.charAt(formatType.length() - 1); - if (formatType.length() >= 2 && - Character.toLowerCase( - formatType.charAt(formatType.length() - 2)) == 't') { - // Date time conversion. - // TODO - continue; - } - switch (last) { - // Booleans. It's okay to pass objects to these; - // it will print "true" if non-null, but it's - // unusual and probably not intended. - case 'b': - case 'B': - valid = type == Boolean.TYPE; - break; - - // Numeric: integer and floats in various formats - case 'x': - case 'X': - case 'd': - case 'o': - case 'e': - case 'E': - case 'f': - case 'g': - case 'G': - case 'a': - case 'A': - valid = type == Integer.TYPE - || type == Float.TYPE; - break; - case 'c': - case 'C': - // Unicode character - valid = type == Character.TYPE; - break; - case 'h': - case 'H': // Hex print of hash code of objects - case 's': - case 'S': - // String. Can pass anything, but warn about - // numbers since you may have meant more - // specific formatting. Use special issue - // explanation for this? - valid = type != Boolean.TYPE && - !type.isAssignableFrom(Number.class); - break; - } - - if (!valid) { - IJavaParser parser = context.parser; - Expression argument = tracker.getArgument(argumentIndex); - Location location = parser.getLocation(context, argument); - Location secondary = handle.resolve(); - secondary.setMessage("Conflicting argument declaration here"); - location.setSecondary(secondary); - - String message = String.format( - "Wrong argument type for formatting argument '#%1$d' " + - "in %2$s: conversion is '%3$s', received %4$s", - i, name, formatType, type.getSimpleName()); - context.report(ARG_TYPES, method, location, message, null); - } - } - } - } - } - } - } - - /** Returns the parent method of the given AST node */ - @Nullable - static lombok.ast.Node getParentMethod(@NonNull lombok.ast.Node node) { - lombok.ast.Node current = node.getParent(); - while (current != null - && !(current instanceof MethodDeclaration) - && !(current instanceof ConstructorDeclaration)) { - current = current.getParent(); - } - - return current; - } - - /** Returns the resource name corresponding to the first argument in the given call */ - static String getResourceForFirstArg(lombok.ast.Node method, lombok.ast.Node call) { - assert call instanceof MethodInvocation || call instanceof ConstructorInvocation; - StringTracker tracker = new StringTracker(method, call, 0); - method.accept(tracker); - String name = tracker.getFormatStringName(); - - return name; - } - - /** Returns the resource name corresponding to the given argument in the given call */ - static String getResourceArg(lombok.ast.Node method, lombok.ast.Node call, int argIndex) { - assert call instanceof MethodInvocation || call instanceof ConstructorInvocation; - StringTracker tracker = new StringTracker(method, call, argIndex); - method.accept(tracker); - String name = tracker.getFormatStringName(); - - return name; - } - - /** - * Given a variable reference, finds the original R.string value corresponding to it. - * For example: - * <pre> - * {@code - * String target = "World"; - * String hello = getResources().getString(R.string.hello); - * String output = String.format(hello, target); - * } - * </pre> - * - * Given the {@code String.format} call, we want to find out what R.string resource - * corresponds to the first argument, in this case {@code R.string.hello}. - * To do this, we look for R.string references, and track those through assignments - * until we reach the target node. - * <p> - * In addition, it also does some primitive type tracking such that it (in some cases) - * can answer questions about the types of variables. This allows it to check whether - * certain argument types are valid. Note however that it does not do full-blown - * type analysis by checking method call signatures and so on. - */ - private static class StringTracker extends ForwardingAstVisitor { - /** Method we're searching within */ - private final lombok.ast.Node mTop; - /** The argument index in the method we're targeting */ - private final int mArgIndex; - /** Map from variable name to corresponding string resource name */ - private final Map<String, String> mMap = new HashMap<String, String>(); - /** Map from variable name to corresponding type */ - private final Map<String, Class<?>> mTypes = new HashMap<String, Class<?>>(); - /** The AST node for the String.format we're interested in */ - private final lombok.ast.Node mTargetNode; - private boolean mDone; - /** - * Result: the name of the string resource being passed to the - * String.format, if any - */ - private String mName; - - public StringTracker(lombok.ast.Node top, lombok.ast.Node targetNode, int argIndex) { - mTop = top; - mArgIndex = argIndex; - mTargetNode = targetNode; - } - - public String getFormatStringName() { - return mName; - } - - /** Returns the argument type of the given formatting argument of the - * target node. Note: This is in the formatting string, which is one higher - * than the String.format parameter number, since the first argument is the - * formatting string itself. - * - * @param argument the argument number - * @return the class (such as {@link Integer#TYPE} etc) or null if not known - */ - public Class<?> getArgumentType(int argument) { - Expression arg = getArgument(argument); - if (arg != null) { - Class<?> type = getType(arg); - if (type != null) { - return type; - } - } - - return null; - } - - public Expression getArgument(int argument) { - if (!(mTargetNode instanceof MethodInvocation)) { - return null; - } - MethodInvocation call = (MethodInvocation) mTargetNode; - StrictListAccessor<Expression, MethodInvocation> args = call.astArguments(); - if (argument >= args.size()) { - return null; - } - - Iterator<Expression> iterator = args.iterator(); - int index = 0; - while (iterator.hasNext()) { - Expression arg = iterator.next(); - if (index++ == argument) { - return arg; - } - } - - return null; - } - - @Override - public boolean visitNode(lombok.ast.Node node) { - if (mDone) { - return true; - } - - return super.visitNode(node); - } - - @Override - public boolean visitVariableReference(VariableReference node) { - if (node.astIdentifier().getDescription().equals("R") && //$NON-NLS-1$ - node.getParent() instanceof Select && - node.getParent().getParent() instanceof Select) { - - // See if we're on the right hand side of an assignment - lombok.ast.Node current = node.getParent().getParent(); - String reference = ((Select) current).astIdentifier().astValue(); - - while (current != mTop && !(current instanceof VariableDefinitionEntry)) { - if (current == mTargetNode) { - mName = reference; - mDone = true; - return false; - } - current = current.getParent(); - } - if (current instanceof VariableDefinitionEntry) { - VariableDefinitionEntry entry = (VariableDefinitionEntry) current; - String variable = entry.astName().astValue(); - mMap.put(variable, reference); - } - } - - return false; - } - - @Nullable - private Expression getTargetArgument() { - Iterator<Expression> iterator; - if (mTargetNode instanceof MethodInvocation) { - iterator = ((MethodInvocation) mTargetNode).astArguments().iterator(); - } else if (mTargetNode instanceof ConstructorInvocation) { - iterator = ((ConstructorInvocation) mTargetNode).astArguments().iterator(); - } else { - return null; - } - int i = 0; - while (i < mArgIndex && iterator.hasNext()) { - iterator.next(); - i++; - } - if (iterator.hasNext()) { - return iterator.next(); - } - - return null; - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (node == mTargetNode) { - Expression arg = getTargetArgument(); - if (arg instanceof VariableReference) { - VariableReference reference = (VariableReference) arg; - String variable = reference.astIdentifier().astValue(); - mName = mMap.get(variable); - mDone = true; - return true; - } - } - - // Is this a getString() call? On a resource object? If so, - // promote the resource argument up to the left hand side - return super.visitMethodInvocation(node); - } - - @Override - public boolean visitConstructorInvocation(ConstructorInvocation node) { - if (node == mTargetNode) { - Expression arg = getTargetArgument(); - if (arg instanceof VariableReference) { - VariableReference reference = (VariableReference) arg; - String variable = reference.astIdentifier().astValue(); - mName = mMap.get(variable); - mDone = true; - return true; - } - } - - // Is this a getString() call? On a resource object? If so, - // promote the resource argument up to the left hand side - return super.visitConstructorInvocation(node); - } - - @Override - public boolean visitVariableDefinitionEntry(VariableDefinitionEntry node) { - String name = node.astName().astValue(); - Expression rhs = node.astInitializer(); - Class<?> type = getType(rhs); - if (type != null) { - mTypes.put(name, type); - } else { - // Make sure we're not visiting the String.format node itself. If you have - // msg = String.format("%1$s", msg) - // then we'd be wiping out the type of "msg" before visiting the - // String.format call! - if (rhs != mTargetNode) { - mTypes.remove(name); - } - } - - return super.visitVariableDefinitionEntry(node); - } - - private Class<?> getType(Expression expression) { - if (expression instanceof VariableReference) { - VariableReference reference = (VariableReference) expression; - String variable = reference.astIdentifier().astValue(); - return mTypes.get(variable); - } else if (expression instanceof MethodInvocation) { - MethodInvocation method = (MethodInvocation) expression; - String methodName = method.astName().astValue(); - if (methodName.equals(GET_STRING_METHOD)) { - return String.class; - } - } else if (expression instanceof StringLiteral) { - return String.class; - } else if (expression instanceof IntegralLiteral) { - return Integer.TYPE; - } else if (expression instanceof FloatingPointLiteral) { - return Float.TYPE; - } else if (expression instanceof CharLiteral) { - return Character.TYPE; - } else if (expression instanceof NullLiteral) { - return Object.class; - } - - return null; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StyleCycleDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StyleCycleDetector.java deleted file mode 100644 index 8ce103b..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/StyleCycleDetector.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_PARENT; -import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; -import static com.android.SdkConstants.TAG_STYLE; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; - -/** - * Checks for cycles in style definitions - */ -public class StyleCycleDetector extends ResourceXmlDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "StyleCycle", //$NON-NLS-1$ - "Looks for cycles in style definitions", - "There should be no cycles in style definitions as this can lead to runtime " + - "exceptions.", - Category.CORRECTNESS, - 8, - Severity.FATAL, - StyleCycleDetector.class, - Scope.RESOURCE_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/ui/themes.html#Inheritance"); //$NON-NLS-1$ - - /** Constructs a new {@link StyleCycleDetector} */ - public StyleCycleDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singleton(TAG_STYLE); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - Attr parentNode = element.getAttributeNode(ATTR_PARENT); - if (parentNode != null) { - String parent = parentNode.getValue(); - String name = element.getAttribute(ATTR_NAME); - if (parent.endsWith(name) && - parent.equals(STYLE_RESOURCE_PREFIX + name)) { - context.report(ISSUE, parentNode, context.getLocation(parentNode), - String.format("Style %1$s should not extend itself", name), null); - } else if (parent.startsWith(STYLE_RESOURCE_PREFIX) - && parent.startsWith(name, STYLE_RESOURCE_PREFIX.length()) - && parent.startsWith(".", STYLE_RESOURCE_PREFIX.length() + name.length())) { - context.report(ISSUE, parentNode, context.getLocation(parentNode), - String.format("Potential cycle: %1$s is the implied parent of %2$s and " + - "this defines the opposite", name, - parent.substring(STYLE_RESOURCE_PREFIX.length())), null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SystemPermissionsDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SystemPermissionsDetector.java deleted file mode 100644 index 21fb6c0..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/SystemPermissionsDetector.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_MANIFEST_XML; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.TAG_USES_PERMISSION; - -import com.android.annotations.NonNull; -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.Issue; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.io.File; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; - -/** - * Checks if an application wants to use permissions that can only be used by - * system applications. - */ -public class SystemPermissionsDetector extends Detector implements Detector.XmlScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ProtectedPermissions", //$NON-NLS-1$ - "Looks for permissions that are only granted to system apps", - - "Permissions with the protection level signature or signatureOrSystem are only " + - "granted to system apps. If an app is a regular non-system app, it will never be " + - "able to use these permissions.", - - Category.CORRECTNESS, - 5, - Severity.ERROR, - SystemPermissionsDetector.class, - EnumSet.of(Scope.MANIFEST)); - - // List of permissions have the protection levels signature or systemOrSignature. - // This list must be sorted alphabetically. - private static final String[] SYSTEM_PERMISSIONS = new String[] { - "android.intent.category.MASTER_CLEAR.permission.C2D_MESSAGE", - "android.permission.ACCESS_CACHE_FILESYSTEM", - "android.permission.ACCESS_CHECKIN_PROPERTIES", - "android.permission.ACCESS_MTP", - "android.permission.ACCESS_SURFACE_FLINGER", - "android.permission.ACCOUNT_MANAGER", - "android.permission.ALLOW_ANY_CODEC_FOR_PLAYBACK", - "android.permission.ASEC_ACCESS", - "android.permission.ASEC_CREATE", - "android.permission.ASEC_DESTROY", - "android.permission.ASEC_MOUNT_UNMOUNT", - "android.permission.ASEC_RENAME", - "android.permission.BACKUP", - "android.permission.BIND_APPWIDGET", - "android.permission.BIND_DEVICE_ADMIN", - "android.permission.BIND_INPUT_METHOD", - "android.permission.BIND_PACKAGE_VERIFIER", - "android.permission.BIND_REMOTEVIEWS", - "android.permission.BIND_TEXT_SERVICE", - "android.permission.BIND_VPN_SERVICE", - "android.permission.BIND_WALLPAPER", - "android.permission.BRICK", - "android.permission.BROADCAST_PACKAGE_REMOVED", - "android.permission.BROADCAST_SMS", - "android.permission.BROADCAST_WAP_PUSH", - "android.permission.CALL_PRIVILEGED", - "android.permission.CHANGE_BACKGROUND_DATA_SETTING", - "android.permission.CHANGE_COMPONENT_ENABLED_STATE", - "android.permission.CLEAR_APP_USER_DATA", - "android.permission.CONFIRM_FULL_BACKUP", - "android.permission.CONNECTIVITY_INTERNAL", - "android.permission.CONTROL_LOCATION_UPDATES", - "android.permission.COPY_PROTECTED_DATA", - "android.permission.CRYPT_KEEPER", - "android.permission.DELETE_CACHE_FILES", - "android.permission.DELETE_PACKAGES", - "android.permission.DEVICE_POWER", - "android.permission.DIAGNOSTIC", - "android.permission.DUMP", - "android.permission.FACTORY_TEST", - "android.permission.FORCE_BACK", - "android.permission.FORCE_STOP_PACKAGES", - "android.permission.GLOBAL_SEARCH", - "android.permission.GLOBAL_SEARCH_CONTROL", - "android.permission.HARDWARE_TEST", - "android.permission.INJECT_EVENTS", - "android.permission.INSTALL_LOCATION_PROVIDER", - "android.permission.INSTALL_PACKAGES", - "android.permission.INTERNAL_SYSTEM_WINDOW", - "android.permission.MANAGE_APP_TOKENS", - "android.permission.MANAGE_NETWORK_POLICY", - "android.permission.MANAGE_USB", - "android.permission.MASTER_CLEAR", - "android.permission.MODIFY_NETWORK_ACCOUNTING", - "android.permission.MODIFY_PHONE_STATE", - "android.permission.MOVE_PACKAGE", - "android.permission.NET_ADMIN", - "android.permission.MODIFY_PHONE_STATE", - "android.permission.PACKAGE_USAGE_STATS", - "android.permission.PACKAGE_VERIFICATION_AGENT", - "android.permission.PERFORM_CDMA_PROVISIONING", - "android.permission.READ_FRAME_BUFFER", - "android.permission.READ_INPUT_STATE", - "android.permission.READ_NETWORK_USAGE_HISTORY", - "android.permission.READ_PRIVILEGED_PHONE_STATE", - "android.permission.REBOOT", - "android.permission.RECEIVE_EMERGENCY_BROADCAST", - "android.permission.REMOVE_TASKS", - "android.permission.RETRIEVE_WINDOW_CONTENT", - "android.permission.SEND_SMS_NO_CONFIRMATION", - "android.permission.SET_ACTIVITY_WATCHER", - "android.permission.SET_ORIENTATION", - "android.permission.SET_POINTER_SPEED", - "android.permission.SET_PREFERRED_APPLICATIONS", - "android.permission.SET_SCREEN_COMPATIBILITY", - "android.permission.SET_TIME", - "android.permission.SET_WALLPAPER_COMPONENT", - "android.permission.SHUTDOWN", - "android.permission.STATUS_BAR", - "android.permission.STATUS_BAR_SERVICE", - "android.permission.STOP_APP_SWITCHES", - "android.permission.UPDATE_DEVICE_STATS", - "android.permission.WRITE_APN_SETTINGS", - "android.permission.WRITE_GSERVICES", - "android.permission.WRITE_MEDIA_STORAGE", - "android.permission.WRITE_SECURE_SETTINGS" - }; - - /** Constructs a new {@link SystemPermissionsDetector} check */ - public SystemPermissionsDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return file.getName().equals(ANDROID_MANIFEST_XML); - } - - // ---- Implements Detector.XmlScanner ---- - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_USES_PERMISSION); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - Attr nameNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_NAME); - if (nameNode != null) { - String permissionName = nameNode.getValue(); - if (Arrays.binarySearch(SYSTEM_PERMISSIONS, permissionName) >= 0) { - context.report(ISSUE, element, context.getLocation(nameNode), - "Permission is only granted to system apps", null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextFieldDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextFieldDetector.java deleted file mode 100644 index a059f5c..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextFieldDetector.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_HINT; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_INPUT_METHOD; -import static com.android.SdkConstants.ATTR_INPUT_TYPE; -import static com.android.SdkConstants.ATTR_PASSWORD; -import static com.android.SdkConstants.ATTR_PHONE_NUMBER; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.EDIT_TEXT; -import static com.android.SdkConstants.ID_PREFIX; -import static com.android.SdkConstants.NEW_ID_PREFIX; - -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; - -/** - * Checks for usability problems in text fields: omitting inputType, or omitting a hint. - */ -public class TextFieldDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "TextFields", //$NON-NLS-1$ - "Looks for text fields missing inputType or hint settings", - - "Providing an `inputType` attribute on a text field improves usability " + - "because depending on the data to be input, optimized keyboards can be shown " + - "to the user (such as just digits and parentheses for a phone number). Similarly," + - "a hint attribute displays a hint to the user for what is expected in the " + - "text field.\n" + - "\n" + - "The lint detector also looks at the `id` of the view, and if the id offers a " + - "hint of the purpose of the field (for example, the `id` contains the phrase " + - "`phone` or `email`), then lint will also ensure that the `inputType` contains " + - "the corresponding type attributes.\n" + - "\n" + - "If you really want to keep the text field generic, you can suppress this warning " + - "by setting `inputType=\"text\"`.", - - Category.USABILITY, - 5, - Severity.WARNING, - TextFieldDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link TextFieldDetector} */ - public TextFieldDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(EDIT_TEXT); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String style = element.getAttribute(ATTR_STYLE); - if (style != null && !style.isEmpty()) { - // The input type might be specified via a style. This will require - // us to track these (similar to what is done for the - // RequiredAttributeDetector to track layout_width and layout_height - // in style declarations). For now, simply ignore these elements - // to avoid producing false positives. - return; - } - - Attr inputTypeNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_INPUT_TYPE); - if (inputTypeNode == null && - !element.hasAttributeNS(ANDROID_URI, ATTR_HINT)) { - // Also make sure the EditText does not set an inputMethod in which case - // an inputType might be provided from the input. - if (element.hasAttributeNS(ANDROID_URI, ATTR_INPUT_METHOD)) { - return; - } - - context.report(ISSUE, element, context.getLocation(element), - "This text field does not specify an inputType or a hint", null); - } - - Attr idNode = element.getAttributeNodeNS(ANDROID_URI, ATTR_ID); - if (idNode == null) { - return; - } - String id = idNode.getValue(); - if (id.isEmpty()) { - return; - } - if (id.startsWith("editText")) { //$NON-NLS-1$ - // Just the default label - return; - } - - String inputType = ""; - if (inputTypeNode != null) { - inputType = inputTypeNode.getValue(); - } - - // TODO: See if the name is just the default names (button1, editText1 etc) - // and if so, do nothing - // TODO: Unit test this - - if (containsWord(id, "phone", true, true)) { //$NON-NLS-1$ - if (!inputType.contains("phone") //$NON-NLS-1$ - && element.getAttributeNodeNS(ANDROID_URI, ATTR_PHONE_NUMBER) == null) { - String message = String.format("The view name (%1$s) suggests this is a phone " - + "number, but it does not include 'phone' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - return; - } - - if (containsWord(id, "width", false, true) - || containsWord(id, "height", false, true) - || containsWord(id, "size", false, true) - || containsWord(id, "length", false, true) - || containsWord(id, "weight", false, true) - || containsWord(id, "number", false, true)) { - if (!inputType.contains("number") && !inputType.contains("phone")) { //$NON-NLS-1$ - String message = String.format("The view name (%1$s) suggests this is a number, " - + "but it does not include a numeric inputType (such as 'numberSigned')", - id); - reportMismatch(context, idNode, inputTypeNode, message); - } - return; - } - - if (containsWord(id, "password", true, true)) { //$NON-NLS-1$ - if (!(inputType.contains("Password")) //$NON-NLS-1$ - && element.getAttributeNodeNS(ANDROID_URI, ATTR_PASSWORD) == null) { - String message = String.format("The view name (%1$s) suggests this is a password, " - + "but it does not include 'textPassword' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - return; - } - - if (containsWord(id, "email", true, true)) { //$NON-NLS-1$ - if (!inputType.contains("Email")) { //$NON-NLS-1$ - String message = String.format("The view name (%1$s) suggests this is an e-mail " - + "address, but it does not include 'textEmail' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - return; - } - - if (endsWith(id, "pin", false, true)) { //$NON-NLS-1$ - if (!(inputType.contains("numberPassword")) //$NON-NLS-1$ - && element.getAttributeNodeNS(ANDROID_URI, ATTR_PASSWORD) == null) { - String message = String.format("The view name (%1$s) suggests this is a password, " - + "but it does not include 'numberPassword' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - return; - } - - if ((containsWord(id, "uri") || containsWord(id, "url")) - && !inputType.contains("textUri")) { - String message = String.format("The view name (%1$s) suggests this is a URI, " - + "but it does not include 'textUri' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - - if ((containsWord(id, "date")) //$NON-NLS-1$ - && !inputType.contains("date")) { //$NON-NLS-1$ - String message = String.format("The view name (%1$s) suggests this is a date, " - + "but it does not include 'date' or 'datetime' in the inputType", id); - reportMismatch(context, idNode, inputTypeNode, message); - } - } - - private static void reportMismatch(XmlContext context, Attr idNode, Attr inputTypeNode, - String message) { - Location location; - if (inputTypeNode != null) { - location = context.getLocation(inputTypeNode); - Location secondary = context.getLocation(idNode); - secondary.setMessage("id defined here"); - location.setSecondary(secondary); - } else { - location = context.getLocation(idNode); - } - context.report(ISSUE, idNode.getOwnerElement(), location, message, null); - } - - /** Returns true if the given sentence contains a given word */ - @VisibleForTesting - static boolean containsWord(String sentence, String word) { - return containsWord(sentence, word, false, false); - } - - /** - * Returns true if the given sentence contains a given word - * @param sentence the full sentence to search within - * @param word the word to look for - * @param allowPrefix if true, allow a prefix match even if the next character - * is in the same word (same case or not an underscore) - * @param allowSuffix if true, allow a suffix match even if the preceding character - * is in the same word (same case or not an underscore) - * @return true if the word is contained in the sentence - */ - @VisibleForTesting - static boolean containsWord(String sentence, String word, boolean allowPrefix, - boolean allowSuffix) { - return indexOfWord(sentence, word, allowPrefix, allowSuffix) != -1; - } - - /** Returns true if the given sentence <b>ends</b> with a given word */ - private static boolean endsWith(String sentence, String word, boolean allowPrefix, - boolean allowSuffix) { - int index = indexOfWord(sentence, word, allowPrefix, allowSuffix); - - if (index != -1) { - return index == sentence.length() - word.length(); - } - - return false; - } - - /** - * Returns the index of the given word in the given sentence, if any. It will match - * across cases, and ignore words that seem to be just a substring in the middle - * of another word. - * - * @param sentence the full sentence to search within - * @param word the word to look for - * @param allowPrefix if true, allow a prefix match even if the next character - * is in the same word (same case or not an underscore) - * @param allowSuffix if true, allow a suffix match even if the preceding character - * is in the same word (same case or not an underscore) - * @return true if the word is contained in the sentence - */ - private static int indexOfWord(String sentence, String word, boolean allowPrefix, - boolean allowSuffix) { - if (sentence.isEmpty()) { - return -1; - } - int wordLength = word.length(); - if (wordLength > sentence.length()) { - return -1; - } - - char firstUpper = Character.toUpperCase(word.charAt(0)); - char firstLower = Character.toLowerCase(firstUpper); - - int start = 0; - if (sentence.startsWith(NEW_ID_PREFIX)) { - start += NEW_ID_PREFIX.length(); - } else if (sentence.startsWith(ID_PREFIX)) { - start += ID_PREFIX.length(); - } - - for (int i = start, n = sentence.length(), m = n - (wordLength - 1); i < m; i++) { - char c = sentence.charAt(i); - if (c == firstUpper || c == firstLower) { - if (sentence.regionMatches(true, i, word, 0, wordLength)) { - if (i <= start && allowPrefix) { - return i; - } - if (i == m - 1 && allowSuffix) { - return i; - } - if (i <= start || (sentence.charAt(i - 1) == '_') - || Character.isUpperCase(c)) { - if (i == m - 1) { - return i; - } - char after = sentence.charAt(i + wordLength); - if (after == '_' || Character.isUpperCase(after)) { - return i; - } - } - } - } - } - - return -1; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextViewDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextViewDetector.java deleted file mode 100644 index 3e519cf..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TextViewDetector.java +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_URI; -import static com.android.SdkConstants.ATTR_AUTO_TEXT; -import static com.android.SdkConstants.ATTR_BUFFER_TYPE; -import static com.android.SdkConstants.ATTR_CAPITALIZE; -import static com.android.SdkConstants.ATTR_CURSOR_VISIBLE; -import static com.android.SdkConstants.ATTR_DIGITS; -import static com.android.SdkConstants.ATTR_EDITABLE; -import static com.android.SdkConstants.ATTR_EDITOR_EXTRAS; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_IME_ACTION_ID; -import static com.android.SdkConstants.ATTR_IME_ACTION_LABEL; -import static com.android.SdkConstants.ATTR_IME_OPTIONS; -import static com.android.SdkConstants.ATTR_INPUT_METHOD; -import static com.android.SdkConstants.ATTR_INPUT_TYPE; -import static com.android.SdkConstants.ATTR_NUMERIC; -import static com.android.SdkConstants.ATTR_PASSWORD; -import static com.android.SdkConstants.ATTR_PHONE_NUMBER; -import static com.android.SdkConstants.ATTR_PRIVATE_IME_OPTIONS; -import static com.android.SdkConstants.ATTR_TEXT; -import static com.android.SdkConstants.ATTR_TEXT_IS_SELECTABLE; -import static com.android.SdkConstants.ATTR_VISIBILITY; -import static com.android.SdkConstants.BUTTON; -import static com.android.SdkConstants.CHECKED_TEXT_VIEW; -import static com.android.SdkConstants.CHECK_BOX; -import static com.android.SdkConstants.RADIO_BUTTON; -import static com.android.SdkConstants.SWITCH; -import static com.android.SdkConstants.TEXT_VIEW; -import static com.android.SdkConstants.TOGGLE_BUTTON; -import static com.android.SdkConstants.VALUE_EDITABLE; -import static com.android.SdkConstants.VALUE_NONE; -import static com.android.SdkConstants.VALUE_TRUE; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; - -import java.util.Arrays; -import java.util.Collection; - -/** - * Checks for cases where a TextView should probably be an EditText instead - */ -public class TextViewDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "TextViewEdits", //$NON-NLS-1$ - "Looks for TextViews being used for input", - - "Using a `<TextView>` to input text is generally an error, you should be " + - "using `<EditText>` instead. `EditText` is a subclass of `TextView`, and some " + - "of the editing support is provided by `TextView`, so it's possible to set " + - "some input-related properties on a `TextView`. However, using a `TextView` " + - "along with input attributes is usually a cut & paste error. To input " + - "text you should be using `<EditText>`." + - "\n" + - "This check also checks subclasses of `TextView`, such as `Button` and `CheckBox`, " + - "since these have the same issue: they should not be used with editable " + - "attributes.", - - Category.CORRECTNESS, - 7, - Severity.WARNING, - TextViewDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Text could be selectable */ - public static final Issue SELECTABLE = Issue.create( - "SelectableText", //$NON-NLS-1$ - "Looks for TextViews which should probably allow their text to be selected", - - "If a `<TextView>` is used to display data, the user might want to copy that " + - "data and paste it elsewhere. To allow this, the `<TextView>` should specify " + - "`android:textIsSelectable=\"true\"`.\n" + - "\n" + - "This lint check looks for TextViews which are likely to be displaying data: " + - "views whose text is set dynamically. This value will be ignored on platforms " + - "older than API 11, so it is okay to set it regardless of your `minSdkVersion`.", - - Category.USABILITY, - 7, - Severity.WARNING, - TextViewDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link TextViewDetector} */ - public TextViewDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TEXT_VIEW, - BUTTON, - TOGGLE_BUTTON, - CHECK_BOX, - RADIO_BUTTON, - CHECKED_TEXT_VIEW, - SWITCH - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (element.getTagName().equals(TEXT_VIEW)) { - if (!element.hasAttributeNS(ANDROID_URI, ATTR_TEXT) - && element.hasAttributeNS(ANDROID_URI, ATTR_ID) - && !element.hasAttributeNS(ANDROID_URI, ATTR_TEXT_IS_SELECTABLE) - && !element.hasAttributeNS(ANDROID_URI, ATTR_VISIBILITY) - && context.getMainProject().getTargetSdk() >= 11) { - context.report(SELECTABLE, element, context.getLocation(element), - "Consider making the text value selectable by specifying " + - "android:textIsSelectable=\"true\"", null); - } - } - - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attribute = (Attr) attributes.item(i); - String name = attribute.getLocalName(); - if (name == null) { - // Attribute not in a namespace; we only care about the android: ones - continue; - } - - boolean isEditAttribute = false; - switch (name.charAt(0)) { - case 'a': { - isEditAttribute = name.equals(ATTR_AUTO_TEXT); - break; - } - case 'b': { - isEditAttribute = name.equals(ATTR_BUFFER_TYPE) && - attribute.getValue().equals(VALUE_EDITABLE); - break; - } - case 'p': { - isEditAttribute = name.equals(ATTR_PASSWORD) - || name.equals(ATTR_PHONE_NUMBER) - || name.equals(ATTR_PRIVATE_IME_OPTIONS); - break; - } - case 'c': { - isEditAttribute = name.equals(ATTR_CAPITALIZE) - || name.equals(ATTR_CURSOR_VISIBLE); - break; - } - case 'd': { - isEditAttribute = name.equals(ATTR_DIGITS); - break; - } - case 'e': { - if (name.equals(ATTR_EDITABLE)) { - isEditAttribute = attribute.getValue().equals(VALUE_TRUE); - } else { - isEditAttribute = name.equals(ATTR_EDITOR_EXTRAS); - } - break; - } - case 'i': { - if (name.equals(ATTR_INPUT_TYPE)) { - String value = attribute.getValue(); - isEditAttribute = !value.isEmpty() && !value.equals(VALUE_NONE); - } else { - isEditAttribute = name.equals(ATTR_INPUT_TYPE) - || name.equals(ATTR_IME_OPTIONS) - || name.equals(ATTR_IME_ACTION_LABEL) - || name.equals(ATTR_IME_ACTION_ID) - || name.equals(ATTR_INPUT_METHOD); - } - break; - } - case 'n': { - isEditAttribute = name.equals(ATTR_NUMERIC); - break; - } - } - - if (isEditAttribute && ANDROID_URI.equals(attribute.getNamespaceURI())) { - Location location = context.getLocation(attribute); - String message; - String view = element.getTagName(); - if (view.equals(TEXT_VIEW)) { - message = String.format( - "Attribute %1$s should not be used with <TextView>: " + - "Change element type to <EditText> ?", attribute.getName()); - } else { - message = String.format( - "Attribute %1$s should not be used with <%2$s>: " + - "intended for editable text widgets", - attribute.getName(), view); - } - context.report(ISSUE, attribute, location, message, null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TitleDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TitleDetector.java deleted file mode 100644 index 5fc5340..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TitleDetector.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_URI; -import static com.android.SdkConstants.ATTR_TITLE; -import static com.android.SdkConstants.ATTR_VISIBLE; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.VALUE_FALSE; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector.JavaScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; - -/** - * Check which makes sure menu items specify a title - */ -public class TitleDetector extends ResourceXmlDetector implements JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "MenuTitle", //$NON-NLS-1$ - "Ensures that all menu items supply a title", - - "From the action bar documentation:\n" + - // u2014: em dash - "\"It's important that you always define android:title for each menu item \u2014 " + - "even if you don't declare that the title appear with the action item \u2014 for " + - "three reasons:\n" + - "\n" + - "* If there's not enough room in the action bar for the action item, the menu " + - "item appears in the overflow menu and only the title appears.\n" + - "* Screen readers for sight-impaired users read the menu item's title.\n" + - "* If the action item appears with only the icon, a user can long-press the item " + - "to reveal a tool-tip that displays the action item's title.\n" + - "The android:icon is always optional, but recommended.", - - Category.USABILITY, - 5, - Severity.WARNING, - TitleDetector.class, - Scope.RESOURCE_FILE_SCOPE).setMoreInfo( - "http://developer.android.com/guide/topics/ui/actionbar.html"); //$NON-NLS-1$ - - /** Constructs a new {@link TitleDetector} */ - public TitleDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.MENU; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - @Nullable - public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_ITEM); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (element.hasAttributeNS(ANDROID_URI, ATTR_TITLE)) { - return; - } - - // TODO: Find out if this is necessary on older versions too. - // I swear I saw it mentioned. - if (context.getMainProject().getTargetSdk() < 11) { - return; - } - - if (VALUE_FALSE.equals(element.getAttributeNS(ANDROID_URI, ATTR_VISIBLE))) { - return; - } - - String message = "Menu items should specify a title"; - context.report(ISSUE, element, context.getLocation(element), message, null); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ToastDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ToastDetector.java deleted file mode 100644 index e245fe8..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ToastDetector.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import java.io.File; -import java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.Expression; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.IntegralLiteral; -import lombok.ast.MethodInvocation; -import lombok.ast.Node; -import lombok.ast.Return; -import lombok.ast.StrictListAccessor; - -/** Detector looking for Toast.makeText() without a corresponding show() call */ -public class ToastDetector extends Detector implements Detector.JavaScanner { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ShowToast", //$NON-NLS-1$ - "Looks for code creating a Toast but forgetting to call show() on it", - - "`Toast.makeText()` creates a `Toast` but does *not* show it. You must call " + - "`show()` on the resulting object to actually make the `Toast` appear.", - - Category.CORRECTNESS, - 6, - Severity.WARNING, - ToastDetector.class, - Scope.JAVA_FILE_SCOPE); - - - /** Constructs a new {@link ToastDetector} check */ - public ToastDetector() { - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - - // ---- Implements JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("makeText"); //$NON-NLS-1$ - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - assert node.astName().astValue().equals("makeText"); - if (node.astOperand() == null) { - // "makeText()" in the code with no operand - return; - } - - String operand = node.astOperand().toString(); - if (!(operand.equals("Toast") || operand.endsWith(".Toast"))) { - return; - } - - // Make sure you pass the right kind of duration: it's not a delay, it's - // LENGTH_SHORT or LENGTH_LONG - // (see http://code.google.com/p/android/issues/detail?id=3655) - StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); - if (args.size() == 3) { - Expression duration = args.last(); - if (duration instanceof IntegralLiteral) { - context.report(ISSUE, duration, context.getLocation(duration), - "Expected duration Toast.LENGTH_SHORT or Toast.LENGTH_LONG, a custom " + - "duration value is not supported", - null); - } - } - - Node method = JavaContext.findSurroundingMethod(node.getParent()); - if (method == null) { - return; - } - - ShowFinder finder = new ShowFinder(node); - method.accept(finder); - if (!finder.isShowCalled()) { - context.report(ISSUE, method, context.getLocation(node), - "Toast created but not shown: did you forget to call show() ?", null); - } - } - - private static class ShowFinder extends ForwardingAstVisitor { - /** The target makeText call */ - private final MethodInvocation mTarget; - /** Whether we've found the show method */ - private boolean mFound; - /** Whether we've seen the target makeText node yet */ - private boolean mSeenTarget; - - private ShowFinder(MethodInvocation target) { - mTarget = target; - } - - @Override - public boolean visitMethodInvocation(MethodInvocation node) { - if (node == mTarget) { - mSeenTarget = true; - } else if ((mSeenTarget || node.astOperand() == mTarget) - && "show".equals(node.astName().astValue())) { //$NON-NLS-1$ - // TODO: Do more flow analysis to see whether we're really calling show - // on the right type of object? - mFound = true; - } - - return true; - } - - @Override - public boolean visitReturn(Return node) { - if (node.astValue() == mTarget) { - // If you just do "return Toast.makeText(...) don't warn - mFound = true; - } - return super.visitReturn(node); - } - - boolean isShowCalled() { - return mFound; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TooManyViewsDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TooManyViewsDetector.java deleted file mode 100644 index 94a9611..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TooManyViewsDetector.java +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Element; - -import java.util.Collection; - -/** - * Checks whether a root FrameLayout can be replaced with a {@code <merge>} tag. - */ -public class TooManyViewsDetector extends LayoutDetector { - /** Issue of having too many views in a single layout */ - public static final Issue TOO_MANY = Issue.create( - "TooManyViews", //$NON-NLS-1$ - "Checks whether a layout has too many views", - "Using too many views in a single layout is bad for " + - "performance. Consider using compound drawables or other tricks for " + - "reducing the number of views in this layout.\n\n" + - "The maximum view count defaults to 80 but can be configured with the " + - "environment variable `ANDROID_LINT_MAX_VIEW_COUNT`.", - Category.PERFORMANCE, - 1, - Severity.WARNING, - TooManyViewsDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Issue of having too deep hierarchies in layouts */ - public static final Issue TOO_DEEP = Issue.create( - "TooDeepLayout", //$NON-NLS-1$ - "Checks whether a layout hierarchy is too deep", - "Layouts with too much nesting is bad for performance. " + - "Consider using a flatter layout (such as `RelativeLayout` or `GridLayout`)." + - "The default maximum depth is 10 but can be configured with the environment " + - "variable `ANDROID_LINT_MAX_DEPTH`.", - Category.PERFORMANCE, - 1, - Severity.WARNING, - TooManyViewsDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - private static final int MAX_VIEW_COUNT; - private static final int MAX_DEPTH; - static { - int maxViewCount = 0; - int maxDepth = 0; - - String countValue = System.getenv("ANDROID_LINT_MAX_VIEW_COUNT"); //$NON-NLS-1$ - if (countValue != null) { - try { - maxViewCount = Integer.parseInt(countValue); - } catch (NumberFormatException e) { - // pass: set to default below - } - } - String depthValue = System.getenv("ANDROID_LINT_MAX_DEPTH"); //$NON-NLS-1$ - if (depthValue != null) { - try { - maxDepth = Integer.parseInt(depthValue); - } catch (NumberFormatException e) { - // pass: set to default below - } - } - if (maxViewCount == 0) { - maxViewCount = 80; - } - if (maxDepth == 0) { - maxDepth = 10; - } - - MAX_VIEW_COUNT = maxViewCount; - MAX_DEPTH = maxDepth; - } - - private int mViewCount; - private int mDepth; - private boolean mWarnedAboutDepth; - - /** Constructs a new {@link TooManyViewsDetector} */ - public TooManyViewsDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - mViewCount = mDepth = 0; - mWarnedAboutDepth = false; - } - - @Override - public Collection<String> getApplicableElements() { - return ALL; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - mViewCount++; - mDepth++; - - if (mDepth == MAX_DEPTH && !mWarnedAboutDepth) { - // Have to record whether or not we've warned since we could have many siblings - // at the max level and we'd warn for each one. No need to do the same thing - // for the view count error since we'll only have view count exactly equal the - // max just once. - mWarnedAboutDepth = true; - String msg = String.format("%1$s has more than %2$d levels, bad for performance", - context.file.getName(), MAX_DEPTH); - context.report(TOO_DEEP, element, context.getLocation(element), msg, null); - } - if (mViewCount == MAX_VIEW_COUNT) { - String msg = String.format("%1$s has more than %2$d views, bad for performance", - context.file.getName(), MAX_VIEW_COUNT); - context.report(TOO_MANY, element, context.getLocation(element), msg, null); - } - } - - @Override - public void visitElementAfter(@NonNull XmlContext context, @NonNull Element element) { - mDepth--; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TranslationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TranslationDetector.java deleted file mode 100644 index 0f892b1..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TranslationDetector.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_PREFIX; -import static com.android.SdkConstants.ATTR_LOCALE; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_TRANSLATABLE; -import static com.android.SdkConstants.STRING_PREFIX; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.TAG_STRING_ARRAY; -import static com.android.SdkConstants.TOOLS_URI; - -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -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.ResourceXmlDetector; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; -import com.android.tools.lint.detector.api.XmlContext; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * Checks for incomplete translations - e.g. keys that are only present in some - * locales but not all. - */ -public class TranslationDetector extends ResourceXmlDetector { - @VisibleForTesting - static boolean sCompleteRegions = - System.getenv("ANDROID_LINT_COMPLETE_REGIONS") != null; //$NON-NLS-1$ - - private static final Pattern LANGUAGE_PATTERN = Pattern.compile("^[a-z]{2}$"); //$NON-NLS-1$ - private static final Pattern REGION_PATTERN = Pattern.compile("^r([A-Z]{2})$"); //$NON-NLS-1$ - - /** Are all translations complete? */ - public static final Issue MISSING = Issue.create( - "MissingTranslation", //$NON-NLS-1$ - "Checks for incomplete translations where not all strings are translated", - "If an application has more than one locale, then all the strings declared in " + - "one language should also be translated in all other languages.\n" + - "\n" + - "If the string should *not* be translated, you can add the attribute " + - "`translatable=\"false\"` on the `<string>` element, or you can define all " + - "your non-translatable strings in a resource file called `donottranslate.xml`. " + - "Or, you can ignore the issue with a `tools:ignore=\"MissingTranslation\"` " + - "attribute.\n" + - "\n" + - "By default this detector allows regions of a language to just provide a " + - "subset of the strings and fall back to the standard language strings. " + - "You can require all regions to provide a full translation by setting the " + - "environment variable `ANDROID_LINT_COMPLETE_REGIONS`.\n" + - "\n" + - "You can tell lint (and other tools) which language is the default language " + - "in your `res/values/` folder by specifying `tools:locale=\"languageCode\"` for " + - "the root `<resources>` element in your resource file. (The `tools` prefix refers " + - "to the namespace declaration `http://schemas.android.com/tools`.)", - Category.MESSAGES, - 8, - Severity.FATAL, - TranslationDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Are there extra translations that are "unused" (appear only in specific languages) ? */ - public static final Issue EXTRA = Issue.create( - "ExtraTranslation", //$NON-NLS-1$ - "Checks for translations that appear to be unused (no default language string)", - "If a string appears in a specific language translation file, but there is " + - "no corresponding string in the default locale, then this string is probably " + - "unused. (It's technically possible that your application is only intended to " + - "run in a specific locale, but it's still a good idea to provide a fallback.).\n" + - "\n" + - "Note that these strings can lead to crashes if the string is looked up on any " + - "locale not providing a translation, so it's important to clean them up.", - Category.MESSAGES, - 6, - Severity.FATAL, - TranslationDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - private Set<String> mNames; - private Set<String> mTranslatedArrays; - private Set<String> mNonTranslatable; - private boolean mIgnoreFile; - private Map<File, Set<String>> mFileToNames; - private Map<File, String> mFileToLocale; - - /** Locations for each untranslated string name. Populated during phase 2, if necessary */ - private Map<String, Location> mMissingLocations; - - /** Locations for each extra translated string name. Populated during phase 2, if necessary */ - private Map<String, Location> mExtraLocations; - - /** Error messages for each untranslated string name. Populated during phase 2, if necessary */ - private Map<String, String> mDescriptions; - - /** Constructs a new {@link TranslationDetector} */ - public TranslationDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_STRING, - TAG_STRING_ARRAY - ); - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - if (context.getDriver().getPhase() == 1) { - mFileToNames = new HashMap<File, Set<String>>(); - } - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - if (context.getPhase() == 1) { - mNames = new HashSet<String>(); - } - - // Convention seen in various projects - mIgnoreFile = context.file.getName().startsWith("donottranslate") //$NON-NLS-1$ - || UnusedResourceDetector.isAnalyticsFile(context); - - if (!context.getProject().getReportIssues()) { - mIgnoreFile = true; - } - } - - @Override - public void afterCheckFile(@NonNull Context context) { - if (context.getPhase() == 1) { - // Store this layout's set of ids for full project analysis in afterCheckProject - if (context.getProject().getReportIssues() && mNames != null) { - mFileToNames.put(context.file, mNames); - - Element root = ((XmlContext) context).document.getDocumentElement(); - if (root != null) { - String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE); - if (locale != null && !locale.isEmpty()) { - if (mFileToLocale == null) { - mFileToLocale = Maps.newHashMap(); - } - mFileToLocale.put(context.file, locale); - } - } - } - - mNames = null; - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - // NOTE - this will look for the presence of translation strings. - // If you create a resource folder but don't actually place a file in it - // we won't detect that, but it seems like a smaller problem. - - checkTranslations(context); - - mFileToNames = null; - - if (mMissingLocations != null || mExtraLocations != null) { - context.getDriver().requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); - } - } else { - assert context.getPhase() == 2; - - reportMap(context, MISSING, mMissingLocations); - reportMap(context, EXTRA, mExtraLocations); - mMissingLocations = null; - mExtraLocations = null; - mDescriptions = null; - } - } - - private void reportMap(Context context, Issue issue, Map<String, Location> map) { - if (map != null) { - for (Map.Entry<String, Location> entry : map.entrySet()) { - Location location = entry.getValue(); - String name = entry.getKey(); - String message = mDescriptions.get(name); - - // We were prepending locations, but we want to prefer the base folders - location = Location.reverse(location); - - context.report(issue, location, message, null); - } - } - } - - private void checkTranslations(Context context) { - // Only one file defining strings? If so, no problems. - Set<File> files = mFileToNames.keySet(); - if (files.size() == 1) { - return; - } - - Set<File> parentFolders = new HashSet<File>(); - for (File file : files) { - parentFolders.add(file.getParentFile()); - } - if (parentFolders.size() == 1) { - // Only one language - no problems. - return; - } - - boolean reportMissing = context.isEnabled(MISSING); - boolean reportExtra = context.isEnabled(EXTRA); - - // res/strings.xml etc - String defaultLanguage = "Default"; - - Map<File, String> parentFolderToLanguage = new HashMap<File, String>(); - for (File parent : parentFolders) { - String name = parent.getName(); - - // Look up the language for this folder. - String language = getLanguage(name); - if (language == null) { - language = defaultLanguage; - } - - parentFolderToLanguage.put(parent, language); - } - - int languageCount = parentFolderToLanguage.values().size(); - if (languageCount <= 1) { - // At most one language -- no problems. - return; - } - - // Merge together the various files building up the translations for each language - Map<String, Set<String>> languageToStrings = - new HashMap<String, Set<String>>(languageCount); - Set<String> allStrings = new HashSet<String>(200); - for (File file : files) { - String language = null; - if (mFileToLocale != null) { - String locale = mFileToLocale.get(file); - if (locale != null) { - int index = locale.indexOf('-'); - if (index != -1) { - locale = locale.substring(0, index); - } - language = locale; - } - } - if (language == null) { - language = parentFolderToLanguage.get(file.getParentFile()); - } - assert language != null : file.getParent(); - Set<String> fileStrings = mFileToNames.get(file); - - Set<String> languageStrings = languageToStrings.get(language); - if (languageStrings == null) { - // We don't need a copy; we're done with the string tables now so we - // can modify them - languageToStrings.put(language, fileStrings); - } else { - languageStrings.addAll(fileStrings); - } - allStrings.addAll(fileStrings); - } - - Set<String> defaultStrings = languageToStrings.get(defaultLanguage); - if (defaultStrings == null) { - defaultStrings = new HashSet<String>(); - } - - // Fast check to see if there's no problem: if the default locale set is the - // same as the all set (meaning there are no extra strings in the other languages) - // then we can quickly determine if everything is okay by just making sure that - // each language defines everything. If that's the case they will all have the same - // string count. - int stringCount = allStrings.size(); - if (stringCount == defaultStrings.size()) { - boolean haveError = false; - for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) { - Set<String> strings = entry.getValue(); - if (stringCount != strings.size()) { - haveError = true; - break; - } - } - if (!haveError) { - return; - } - } - - // Do we need to resolve fallback strings for regions that only define a subset - // of the strings in the language and fall back on the main language for the rest? - if (!sCompleteRegions) { - for (String l : languageToStrings.keySet()) { - if (l.indexOf('-') != -1) { - // Yes, we have regions. Merge all base language string names into each region. - for (Map.Entry<String, Set<String>> entry : languageToStrings.entrySet()) { - Set<String> strings = entry.getValue(); - if (stringCount != strings.size()) { - String languageRegion = entry.getKey(); - int regionIndex = languageRegion.indexOf('-'); - if (regionIndex != -1) { - String language = languageRegion.substring(0, regionIndex); - Set<String> fallback = languageToStrings.get(language); - if (fallback != null) { - strings.addAll(fallback); - } - } - } - } - // We only need to do this once; when we see the first region we know - // we need to do it; once merged we can bail - break; - } - } - } - - List<String> languages = new ArrayList<String>(languageToStrings.keySet()); - Collections.sort(languages); - for (String language : languages) { - Set<String> strings = languageToStrings.get(language); - if (defaultLanguage.equals(language)) { - continue; - } - - // if strings.size() == stringCount, then this language is defining everything, - // both all the default language strings and the union of all extra strings - // defined in other languages, so there's no problem. - if (stringCount != strings.size()) { - if (reportMissing) { - Set<String> difference = Sets.difference(defaultStrings, strings); - if (!difference.isEmpty()) { - if (mMissingLocations == null) { - mMissingLocations = new HashMap<String, Location>(); - } - if (mDescriptions == null) { - mDescriptions = new HashMap<String, String>(); - } - - for (String s : difference) { - mMissingLocations.put(s, null); - String message = mDescriptions.get(s); - if (message == null) { - message = String.format("\"%1$s\" is not translated in %2$s", - s, language); - } else { - message = message + ", " + language; - } - mDescriptions.put(s, message); - } - } - } - - if (reportExtra) { - Set<String> difference = Sets.difference(strings, defaultStrings); - if (!difference.isEmpty()) { - if (mExtraLocations == null) { - mExtraLocations = new HashMap<String, Location>(); - } - if (mDescriptions == null) { - mDescriptions = new HashMap<String, String>(); - } - - for (String s : difference) { - if (mTranslatedArrays != null && mTranslatedArrays.contains(s)) { - continue; - } - mExtraLocations.put(s, null); - String message = String.format( - "\"%1$s\" is translated here but not found in default locale", s); - mDescriptions.put(s, message); - } - } - } - } - } - } - - /** Look up the language for the given folder name */ - private static String getLanguage(String name) { - String[] segments = name.split("-"); //$NON-NLS-1$ - - // TODO: To get an accurate answer, this should later do a - // FolderConfiguration.getConfig(String[] folderSegments) - // to obtain a FolderConfiguration, then call - // getLanguageQualifier() on it, and if not null, call getValue() to get the - // actual language value. - // However, we don't have sdk_common on the build path for lint, so for now - // use a simple guess about what constitutes a language qualifier here: - - String language = null; - for (String segment : segments) { - // Language - if (language == null && segment.length() == 2 - && LANGUAGE_PATTERN.matcher(segment).matches()) { - language = segment; - } - - // Add in region - if (language != null && segment.length() == 3 - && REGION_PATTERN.matcher(segment).matches()) { - language = language + '-' + segment; - break; - } - } - - return language; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (mIgnoreFile) { - return; - } - - Attr attribute = element.getAttributeNode(ATTR_NAME); - - if (context.getPhase() == 2) { - // Just locating names requested in the {@link #mLocations} map - if (attribute == null) { - return; - } - String name = attribute.getValue(); - if (mMissingLocations != null && mMissingLocations.containsKey(name)) { - String language = getLanguage(context.file.getParentFile().getName()); - if (language == null) { - if (context.getDriver().isSuppressed(MISSING, element)) { - mMissingLocations.remove(name); - return; - } - - Location location = context.getLocation(attribute); - location.setClientData(element); - location.setSecondary(mMissingLocations.get(name)); - mMissingLocations.put(name, location); - } - } - if (mExtraLocations != null && mExtraLocations.containsKey(name)) { - if (context.getDriver().isSuppressed(EXTRA, element)) { - mExtraLocations.remove(name); - return; - } - Location location = context.getLocation(attribute); - location.setClientData(element); - location.setMessage("Also translated here"); - location.setSecondary(mExtraLocations.get(name)); - mExtraLocations.put(name, location); - } - return; - } - - assert context.getPhase() == 1; - if (attribute == null || attribute.getValue().isEmpty()) { - context.report(MISSING, element, context.getLocation(element), - "Missing name attribute in <string> declaration", null); - } else { - String name = attribute.getValue(); - - Attr translatable = element.getAttributeNode(ATTR_TRANSLATABLE); - if (translatable != null && !Boolean.valueOf(translatable.getValue())) { - String l = LintUtils.getLocaleAndRegion(context.file.getParentFile().getName()); - if (l != null) { - context.report(EXTRA, translatable, context.getLocation(translatable), - "Non-translatable resources should only be defined in the base " + - "values/ folder", null); - } else { - if (mNonTranslatable == null) { - mNonTranslatable = new HashSet<String>(); - } - mNonTranslatable.add(name); - } - return; - } - - if (element.getTagName().equals(TAG_STRING_ARRAY) && - allItemsAreReferences(element)) { - // No need to provide translations for string arrays where all - // the children items are defined as translated string resources, - // e.g. - // <string-array name="foo"> - // <item>@string/item1</item> - // <item>@string/item2</item> - // </string-array> - // However, we need to remember these names such that we don't consider - // these arrays "extra" if one of the *translated* versions of the array - // perform an inline translation of an array item - if (mTranslatedArrays == null) { - mTranslatedArrays = new HashSet<String>(); - } - mTranslatedArrays.add(name); - return; - } - - // Check for duplicate name definitions? No, because there can be - // additional customizations like product= - //if (mNames.contains(name)) { - // context.mClient.report(ISSUE, context.getLocation(attribute), - // String.format("Duplicate name %1$s, already defined earlier in this file", - // name)); - //} - - mNames.add(name); - - if (mNonTranslatable != null && mNonTranslatable.contains(name)) { - String message = String.format("The resource string \"%1$s\" has been marked as " + - "translatable=\"false\"", name); - context.report(EXTRA, attribute, context.getLocation(attribute), message, null); - } - - // TBD: Also make sure that the strings are not empty or placeholders? - } - } - - private static boolean allItemsAreReferences(Element element) { - assert element.getTagName().equals(TAG_STRING_ARRAY); - NodeList childNodes = element.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node item = childNodes.item(i); - if (item.getNodeType() == Node.ELEMENT_NODE && - TAG_ITEM.equals(item.getNodeName())) { - NodeList itemChildren = item.getChildNodes(); - for (int j = 0, m = itemChildren.getLength(); j < m; j++) { - Node valueNode = itemChildren.item(j); - if (valueNode.getNodeType() == Node.TEXT_NODE) { - String value = valueNode.getNodeValue().trim(); - if (!value.startsWith(ANDROID_PREFIX) - && !value.startsWith(STRING_PREFIX)) { - return false; - } - } - } - } - } - - return true; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoDetector.java deleted file mode 100644 index b9b889f..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoDetector.java +++ /dev/null @@ -1,418 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_LOCALE; -import static com.android.SdkConstants.FD_RES_VALUES; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.TOOLS_URI; -import static com.android.tools.lint.checks.TypoLookup.isLetter; -import static com.google.common.base.Objects.equal; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.google.common.base.Charsets; -import com.google.common.base.Splitter; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * Check which looks for likely typos in Strings. - * <p> - * TODO: - * <ul> - * <li> Add check of Java String literals too! - * <li> Add support for <b>additional</b> languages. The typo detector is now - * multilingual and looks for typos-*locale*.txt files to use. However, - * we need to seed it with additional typo databases. I did some searching - * and came up with some alternatives. Here's the strategy I used: - * Used Google Translate to translate "Wikipedia Common Misspellings", and - * then I went to google.no, google.fr etc searching with that translation, and - * came up with what looks like wikipedia language local lists of typos. - * This is how I found the Norwegian one for example: - * <br> - * http://no.wikipedia.org/wiki/Wikipedia:Liste_over_alminnelige_stavefeil/Maskinform - * <br> - * Here are some additional possibilities not yet processed: - * <ul> - * <li> French: http://fr.wikipedia.org/wiki/Wikip%C3%A9dia:Liste_de_fautes_d'orthographe_courantes - * (couldn't find a machine-readable version there?) - * <li> Swedish: - * http://sv.wikipedia.org/wiki/Wikipedia:Lista_%C3%B6ver_vanliga_spr%C3%A5kfel - * (couldn't find a machine-readable version there?) - * <li> German - * http://de.wikipedia.org/wiki/Wikipedia:Liste_von_Tippfehlern/F%C3%BCr_Maschinen - * </ul> - * <li> Consider also digesting files like - * http://sv.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/Typos - * See http://en.wikipedia.org/wiki/Wikipedia:AutoWikiBrowser/User_manual. - * </ul> - */ -public class TypoDetector extends ResourceXmlDetector { - @Nullable private TypoLookup mLookup; - @Nullable private String mLastLanguage; - @Nullable private String mLastRegion; - @Nullable private String mLanguage; - @Nullable private String mRegion; - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "Typos", //$NON-NLS-1$ - "Looks for typos in messages", - - "This check looks through the string definitions, and if it finds any words " + - "that look like likely misspellings, they are flagged.", - Category.MESSAGES, - 7, - Severity.WARNING, - TypoDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new detector */ - public TypoDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - /** Look up the locale and region from the given parent folder name and store it - * in {@link #mLanguage} and {@link #mRegion} */ - private void initLocale(@NonNull String parent) { - mLanguage = null; - mRegion = null; - - if (parent.equals(FD_RES_VALUES)) { - return; - } - - for (String qualifier : Splitter.on('-').split(parent)) { - int qualifierLength = qualifier.length(); - if (qualifierLength == 2) { - char first = qualifier.charAt(0); - char second = qualifier.charAt(1); - if (first >= 'a' && first <= 'z' && second >= 'a' && second <= 'z') { - mLanguage = qualifier; - } - } else if (qualifierLength == 3 && qualifier.charAt(0) == 'r') { - char first = qualifier.charAt(1); - char second = qualifier.charAt(2); - if (first >= 'A' && first <= 'Z' && second >= 'A' && second <= 'Z') { - mRegion = new String(new char[] { first, second }); // Don't include the "r" - } - break; - } - } - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - initLocale(context.file.getParentFile().getName()); - if (mLanguage == null) { - // Check to see if the user has specified the language for this folder - // using a tools:locale attribute - if (context instanceof XmlContext) { - Element root = ((XmlContext) context).document.getDocumentElement(); - if (root != null) { - String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE); - if (locale != null && !locale.isEmpty()) { - initLocale(FD_RES_VALUES + '-' + locale); - } - } - } - - if (mLanguage == null) { - mLanguage = "en"; //$NON-NLS-1$ - } - } - - if (!equal(mLastLanguage, mLanguage) || !equal(mLastRegion, mRegion)) { - mLookup = TypoLookup.get(context.getClient(), mLanguage, mRegion); - mLastLanguage = mLanguage; - mLastRegion = mRegion; - } - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.NORMAL; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList(TAG_STRING); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (mLookup == null) { - return; - } - - visit(context, element); - } - - private void visit(XmlContext context, Node node) { - if (node.getNodeType() == Node.TEXT_NODE) { - // TODO: Figure out how to deal with entities - check(context, node, node.getNodeValue()); - } else { - NodeList children = node.getChildNodes(); - for (int i = 0, n = children.getLength(); i < n; i++) { - visit(context, children.item(i)); - } - } - } - - private void check(XmlContext context, Node node, String text) { - int max = text.length(); - int index = 0; - boolean checkedTypos = false; - while (index < max) { - for (; index < max; index++) { - char c = text.charAt(index); - if (c == '\\') { - index++; - continue; - } else if (Character.isLetter(c)) { - break; - } - } - if (index >= max) { - return; - } - int begin = index; - for (; index < max; index++) { - char c = text.charAt(index); - if (c == '\\') { - index++; - break; - } else if (!Character.isLetter(c)) { - break; - } else if (text.charAt(index) >= 0x80) { - // Switch to UTF-8 handling for this string - if (checkedTypos) { - // If we've already checked words we may have reported typos - // so create a substring from the current word and on. - byte[] utf8Text = text.substring(begin).getBytes(Charsets.UTF_8); - check(context, node, utf8Text, 0, utf8Text.length, text, begin); - } else { - // If all we've done so far is skip whitespace (common scenario) - // then no need to substring the text, just re-search with the - // UTF-8 routines - byte[] utf8Text = text.getBytes(Charsets.UTF_8); - check(context, node, utf8Text, 0, utf8Text.length, text, 0); - } - return; - } - } - - int end = index; - checkedTypos = true; - List<String> replacements = mLookup.getTypos(text, begin, end); - if (replacements != null) { - reportTypo(context, node, text, begin, replacements); - } - - index = end + 1; - } - } - - private void check(XmlContext context, Node node, byte[] utf8Text, - int byteStart, int byteEnd, String text, int charStart) { - int index = byteStart; - while (index < byteEnd) { - // Find beginning of word - while (index < byteEnd) { - byte b = utf8Text[index]; - if (b == '\\') { - index++; - charStart++; - if (index < byteEnd) { - b = utf8Text[index]; - } - } else if (isLetter(b)) { - break; - } - index++; - if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) { - // First characters in UTF-8 are always ASCII (0 high bit) or 11XXXXXX - charStart++; - } - } - - if (index >= byteEnd) { - return; - } - int charEnd = charStart; - int begin = index; - - // Find end of word. Unicode has the nice property that even 2nd, 3rd and 4th - // bytes won't match these ASCII characters (because the high bit must be set there) - while (index < byteEnd) { - byte b = utf8Text[index]; - if (b == '\\') { - index++; - charEnd++; - if (index < byteEnd) { - b = utf8Text[index++]; - if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) { - charEnd++; - } - } - break; - } else if (!isLetter(b)) { - break; - } - index++; - if ((b & 0x80) == 0 || (b & 0xC0) == 0xC0) { - // First characters in UTF-8 are always ASCII (0 high bit) or 11XXXXXX - charEnd++; - } - } - - int end = index; - List<String> replacements = mLookup.getTypos(utf8Text, begin, end); - if (replacements != null) { - reportTypo(context, node, text, charStart, replacements); - } - - charStart = charEnd; - } - } - - /** Report the typo found at the given offset and suggest the given replacements */ - private static void reportTypo(XmlContext context, Node node, String text, int begin, - List<String> replacements) { - if (replacements.size() < 2) { - return; - } - - String typo = replacements.get(0); - String word = text.substring(begin, begin + typo.length()); - - String first = null; - String message; - - boolean isCapitalized = Character.isUpperCase(word.charAt(0)); - StringBuilder sb = new StringBuilder(40); - for (int i = 1, n = replacements.size(); i < n; i++) { - String replacement = replacements.get(i); - if (first == null) { - first = replacement; - } - if (sb.length() > 0) { - sb.append(" or "); - } - sb.append('"'); - if (isCapitalized) { - sb.append(Character.toUpperCase(replacement.charAt(0))); - sb.append(replacement.substring(1)); - } else { - sb.append(replacement); - } - sb.append('"'); - } - - if (first != null && first.equalsIgnoreCase(word)) { - if (first.equals(word)) { - return; - } - message = String.format( - "\"%1$s\" is usually capitalized as \"%2$s\"", - word, first); - } else { - message = String.format( - "\"%1$s\" is a common misspelling; did you mean %2$s ?", - word, sb.toString()); - } - - int end = begin + word.length(); - context.report(ISSUE, node, context.getLocation(node, begin, end), message, null); - } - - /** Returns the suggested replacements, if any, for the given typo. The error - * message <b>must</b> be one supplied by lint. - * - * @param errorMessage the error message - * @return a list of replacement words suggested by the error message - */ - @Nullable - public static List<String> getSuggestions(@NonNull String errorMessage) { - // The words are all in quotes; the first word is the misspelling, - // the other words are the suggested replacements - List<String> words = new ArrayList<String>(); - // Skip the typo - int index = errorMessage.indexOf('"'); - index = errorMessage.indexOf('"', index + 1); - index++; - - while (true) { - index = errorMessage.indexOf('"', index); - if (index == -1) { - break; - } - index++; - int start = index; - index = errorMessage.indexOf('"', index); - if (index == -1) { - index = errorMessage.length(); - } - words.add(errorMessage.substring(start, index)); - index++; - } - - return words; - } - - /** - * Returns the typo word in the error message from this detector - * - * @param errorMessage the error message produced earlier by this detector - * @return the typo - */ - @Nullable - public static String getTypo(@NonNull String errorMessage) { - // The words are all in quotes - int index = errorMessage.indexOf('"'); - int start = index + 1; - index = errorMessage.indexOf('"', start); - if (index != -1) { - return errorMessage.substring(start, index); - } - - return null; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoLookup.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoLookup.java deleted file mode 100644 index 7d9d8f2..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypoLookup.java +++ /dev/null @@ -1,785 +0,0 @@ -/* - * Copyright (C) 2012 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.DOT_XML; -import static com.android.tools.lint.detector.api.LintUtils.assertionsEnabled; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.annotations.VisibleForTesting; -import com.android.tools.lint.client.api.LintClient; -import com.android.tools.lint.detector.api.LintUtils; -import com.google.common.base.Charsets; -import com.google.common.base.Splitter; -import com.google.common.io.Files; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel.MapMode; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; -import java.util.WeakHashMap; - -/** - * Database of common typos / misspellings. - */ -public class TypoLookup { - private static final TypoLookup NONE = new TypoLookup(); - - /** String separating misspellings and suggested replacements in the text file */ - private static final String WORD_SEPARATOR = "->"; //$NON-NLS-1$ - - /** Relative path to the typos database file within the Lint installation */ - private static final String XML_FILE_PATH = "tools/support/typos-%1$s.txt"; //$NON-NLS-1$ - private static final String FILE_HEADER = "Typo database used by Android lint\000"; - private static final int BINARY_FORMAT_VERSION = 2; - private static final boolean DEBUG_FORCE_REGENERATE_BINARY = false; - private static final boolean DEBUG_SEARCH = false; - private static final boolean WRITE_STATS = false; - /** Default size to reserve for each API entry when creating byte buffer to build up data */ - private static final int BYTES_PER_ENTRY = 28; - - private final LintClient mClient; - private final File mXmlFile; - private final File mBinaryFile; - private byte[] mData; - private int[] mIndices; - private int mWordCount; - - private static final WeakHashMap<String, TypoLookup> sInstanceMap = - new WeakHashMap<String, TypoLookup>(); - - /** - * Returns an instance of the Typo database for the given locale - * - * @param client the client to associate with this database - used only for - * logging. The database object may be shared among repeated - * invocations, and in that case client used will be the one - * originally passed in. In other words, this parameter may be - * ignored if the client created is not new. - * @param locale the locale to look up a typo database for (should be a - * language code (ISO 639-1, two lowercase character names) - * @param region the region to look up a typo database for (should be a two - * letter ISO 3166-1 alpha-2 country code in upper case) language - * code - * @return a (possibly shared) instance of the typo database, or null if its - * data can't be found - */ - @Nullable - public static TypoLookup get(@NonNull LintClient client, @NonNull String locale, - @Nullable String region) { - synchronized (TypoLookup.class) { - String key = locale; - - if (region != null) { - // Allow for region-specific dictionaries. See for example - // http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences - assert region.length() == 2 - && Character.isUpperCase(region.charAt(0)) - && Character.isUpperCase(region.charAt(1)) : region; - // Look for typos-en-rUS.txt etc - key = locale + 'r' + region; - } - - TypoLookup db = sInstanceMap.get(key); - if (db == null) { - String path = String.format(XML_FILE_PATH, key); - File file = client.findResource(path); - if (file == null) { - // AOSP build environment? - String build = System.getenv("ANDROID_BUILD_TOP"); //$NON-NLS-1$ - if (build != null) { - file = new File(build, ("sdk/files/" //$NON-NLS-1$ - + path.substring(path.lastIndexOf('/') + 1)) - .replace('/', File.separatorChar)); - } - } - - if (file == null || !file.exists()) { - if (region != null) { - // Fall back to the generic locale (non-region-specific) database - return get(client, locale, null); - } - db = NONE; - } else { - db = get(client, file); - assert db != null : file; - } - sInstanceMap.put(key, db); - } - - if (db == NONE) { - return null; - } else { - return db; - } - } - } - - /** - * Returns an instance of the typo database - * - * @param client the client to associate with this database - used only for - * logging - * @param xmlFile the XML file containing configuration data to use for this - * database - * @return a (possibly shared) instance of the typo database, or null - * if its data can't be found - */ - @Nullable - private static TypoLookup get(LintClient client, File xmlFile) { - if (!xmlFile.exists()) { - client.log(null, "The typo database file %1$s does not exist", xmlFile); - return null; - } - - String name = xmlFile.getName(); - if (LintUtils.endsWith(name, DOT_XML)) { - name = name.substring(0, name.length() - DOT_XML.length()); - } - File cacheDir = client.getCacheDir(true/*create*/); - if (cacheDir == null) { - cacheDir = xmlFile.getParentFile(); - } - - File binaryData = new File(cacheDir, name - // Incorporate version number in the filename to avoid upgrade filename - // conflicts on Windows (such as issue #26663) - + '-' + BINARY_FORMAT_VERSION + ".bin"); //$NON-NLS-1$ - - if (DEBUG_FORCE_REGENERATE_BINARY) { - System.err.println("\nTemporarily regenerating binary data unconditionally \nfrom " - + xmlFile + "\nto " + binaryData); - if (!createCache(client, xmlFile, binaryData)) { - return null; - } - } else if (!binaryData.exists() || binaryData.lastModified() < xmlFile.lastModified()) { - if (!createCache(client, xmlFile, binaryData)) { - return null; - } - } - - if (!binaryData.exists()) { - client.log(null, "The typo database file %1$s does not exist", binaryData); - return null; - } - - return new TypoLookup(client, xmlFile, binaryData); - } - - private static boolean createCache(LintClient client, File xmlFile, File binaryData) { - long begin = 0; - if (WRITE_STATS) { - begin = System.currentTimeMillis(); - } - - // Read in data - List<String> lines; - try { - lines = Files.readLines(xmlFile, Charsets.UTF_8); - } catch (IOException e) { - client.log(e, "Can't read typo database file"); - return false; - } - - if (WRITE_STATS) { - long end = System.currentTimeMillis(); - System.out.println("Reading data structures took " + (end - begin) + " ms)"); - } - - try { - writeDatabase(binaryData, lines); - return true; - } catch (IOException ioe) { - client.log(ioe, "Can't write typo cache file"); - } - - return false; - } - - /** Use one of the {@link #get} factory methods instead */ - private TypoLookup( - @NonNull LintClient client, - @NonNull File xmlFile, - @Nullable File binaryFile) { - mClient = client; - mXmlFile = xmlFile; - mBinaryFile = binaryFile; - - if (binaryFile != null) { - readData(); - } - } - - private TypoLookup() { - mClient = null; - mXmlFile = null; - mBinaryFile = null; - } - - private void readData() { - if (!mBinaryFile.exists()) { - mClient.log(null, "%1$s does not exist", mBinaryFile); - return; - } - long start = System.currentTimeMillis(); - try { - MappedByteBuffer buffer = Files.map(mBinaryFile, MapMode.READ_ONLY); - assert buffer.order() == ByteOrder.BIG_ENDIAN; - - // First skip the header - byte[] expectedHeader = FILE_HEADER.getBytes(Charsets.US_ASCII); - buffer.rewind(); - for (int offset = 0; offset < expectedHeader.length; offset++) { - if (expectedHeader[offset] != buffer.get()) { - mClient.log(null, "Incorrect file header: not an typo database cache " + - "file, or a corrupt cache file"); - return; - } - } - - // Read in the format number - if (buffer.get() != BINARY_FORMAT_VERSION) { - // Force regeneration of new binary data with up to date format - if (createCache(mClient, mXmlFile, mBinaryFile)) { - readData(); // Recurse - } - - return; - } - - mWordCount = buffer.getInt(); - - // Read in the word table indices; - int count = mWordCount; - int[] offsets = new int[count]; - - // Another idea: I can just store the DELTAS in the file (and add them up - // when reading back in) such that it takes just ONE byte instead of four! - - for (int i = 0; i < count; i++) { - offsets[i] = buffer.getInt(); - } - - // No need to read in the rest -- we'll just keep the whole byte array in memory - // TODO: Make this code smarter/more efficient. - int size = buffer.limit(); - byte[] b = new byte[size]; - buffer.rewind(); - buffer.get(b); - mData = b; - mIndices = offsets; - - // TODO: We only need to keep the data portion here since we've initialized - // the offset array separately. - // TODO: Investigate (profile) accessing the byte buffer directly instead of - // accessing a byte array. - } catch (IOException e) { - mClient.log(e, null); - } - if (WRITE_STATS) { - long end = System.currentTimeMillis(); - System.out.println("\nRead typo database in " + (end - start) - + " milliseconds."); - System.out.println("Size of data table: " + mData.length + " bytes (" - + Integer.toString(mData.length/1024) + "k)\n"); - } - } - - /** See the {@link #readData()} for documentation on the data format. */ - private static void writeDatabase(File file, List<String> lines) throws IOException { - /* - * 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - * as ASCII characters. The purpose of the header is to identify what the file - * is for, for anyone attempting to open the file. - * 2. A file version number. If the binary file does not match the reader's expected - * version, it can ignore it (and regenerate the cache from XML). - */ - - // Drop comments etc - List<String> words = new ArrayList<String>(lines.size()); - for (String line : lines) { - if (!line.isEmpty() && Character.isLetter(line.charAt(0))) { - int end = line.indexOf(WORD_SEPARATOR); - if (end == -1) { - end = line.trim().length(); - } - String typo = line.substring(0, end).trim(); - String replacements = line.substring(end + WORD_SEPARATOR.length()).trim(); - if (replacements.isEmpty()) { - // We don't support empty replacements - continue; - } - String combined = typo + (char) 0 + replacements; - - words.add(combined); - } - } - - byte[][] wordArrays = new byte[words.size()][]; - for (int i = 0, n = words.size(); i < n; i++) { - String word = words.get(i); - wordArrays[i] = word.getBytes(Charsets.UTF_8); - } - // Sort words, using our own comparator to ensure that it matches the - // binary search in getTypos() - Comparator<byte[]> comparator = new Comparator<byte[]>() { - @Override - public int compare(byte[] o1, byte[] o2) { - return TypoLookup.compare(o1, 0, (byte) 0, o2, 0, o2.length); - } - }; - Arrays.sort(wordArrays, comparator); - - int entryCount = wordArrays.length; - int capacity = entryCount * BYTES_PER_ENTRY; - ByteBuffer buffer = ByteBuffer.allocate(capacity); - buffer.order(ByteOrder.BIG_ENDIAN); - // 1. A file header, which is the exact contents of {@link FILE_HEADER} encoded - // as ASCII characters. The purpose of the header is to identify what the file - // is for, for anyone attempting to open the file. - buffer.put(FILE_HEADER.getBytes(Charsets.US_ASCII)); - - // 2. A file version number. If the binary file does not match the reader's expected - // version, it can ignore it (and regenerate the cache from XML). - buffer.put((byte) BINARY_FORMAT_VERSION); - - // 3. The number of words [1 int] - buffer.putInt(entryCount); - - // 4. Word offset table (one integer per word, pointing to the byte offset in the - // file (relative to the beginning of the file) where each word begins. - // The words are always sorted alphabetically. - int wordOffsetTable = buffer.position(); - - // Reserve enough room for the offset table here: we will backfill it with pointers - // as we're writing out the data structures below - for (int i = 0, n = entryCount; i < n; i++) { - buffer.putInt(0); - } - - int nextEntry = buffer.position(); - int nextOffset = wordOffsetTable; - - // 7. Word entry table. Each word entry consists of the word, followed by the byte 0 - // as a terminator, followed by a comma separated list of suggestions (which - // may be empty), or a final 0. - for (int i = 0; i < entryCount; i++) { - byte[] word = wordArrays[i]; - buffer.position(nextOffset); - buffer.putInt(nextEntry); - nextOffset = buffer.position(); - buffer.position(nextEntry); - - buffer.put(word); // already embeds 0 to separate typo from words - buffer.put((byte) 0); - - nextEntry = buffer.position(); - } - - int size = buffer.position(); - assert size <= buffer.limit(); - buffer.mark(); - - if (WRITE_STATS) { - System.out.println("Wrote " + words.size() + " word entries"); - System.out.print("Actual binary size: " + size + " bytes"); - System.out.println(String.format(" (%.1fM)", size/(1024*1024.f))); - - System.out.println("Allocated size: " + (entryCount * BYTES_PER_ENTRY) + " bytes"); - System.out.println("Required bytes per entry: " + (size/ entryCount) + " bytes"); - } - - // Now dump this out as a file - // There's probably an API to do this more efficiently; TODO: Look into this. - byte[] b = new byte[size]; - buffer.rewind(); - buffer.get(b); - FileOutputStream output = Files.newOutputStreamSupplier(file).getOutput(); - output.write(b); - output.close(); - } - - // For debugging only - private String dumpEntry(int offset) { - if (DEBUG_SEARCH) { - int end = offset; - while (mData[end] != 0) { - end++; - } - return new String(mData, offset, end - offset, Charsets.UTF_8); - } else { - return "<disabled>"; //$NON-NLS-1$ - } - } - - /** Comparison function: *only* used for ASCII strings */ - @VisibleForTesting - static int compare(byte[] data, int offset, byte terminator, CharSequence s, - int begin, int end) { - int i = offset; - int j = begin; - for (; ; i++, j++) { - byte b = data[i]; - if (b == ' ') { - // We've matched up to the space in a split-word typo, such as - // in German all zu=>allzu; here we've matched just past "all". - // Rather than terminating, attempt to continue in the buffer. - if (j == end) { - int max = s.length(); - if (end < max && s.charAt(end) == ' ') { - // Find next word - for (; end < max; end++) { - char c = s.charAt(end); - if (!Character.isLetter(c)) { - if (c == ' ' && end == j) { - continue; - } - break; - } - } - } - } - } - - if (j == end) { - break; - } - - if (b == '*') { - // Glob match (only supported at the end) - return 0; - } - char c = s.charAt(j); - byte cb = (byte) c; - int delta = b - cb; - if (delta != 0) { - cb = (byte) Character.toLowerCase(c); - if (b != cb) { - // Ensure that it has the right sign - b = (byte) Character.toLowerCase(b); - delta = b - cb; - if (delta != 0) { - return delta; - } - } - } - } - - return data[i] - terminator; - } - - /** Comparison function used for general UTF-8 encoded strings */ - @VisibleForTesting - static int compare(byte[] data, int offset, byte terminator, byte[] s, - int begin, int end) { - int i = offset; - int j = begin; - for (; ; i++, j++) { - byte b = data[i]; - if (b == ' ') { - // We've matched up to the space in a split-word typo, such as - // in German all zu=>allzu; here we've matched just past "all". - // Rather than terminating, attempt to continue in the buffer. - // We've matched up to the space in a split-word typo, such as - // in German all zu=>allzu; here we've matched just past "all". - // Rather than terminating, attempt to continue in the buffer. - if (j == end) { - int max = s.length; - if (end < max && s[end] == ' ') { - // Find next word - for (; end < max; end++) { - byte cb = s[end]; - if (!isLetter(cb)) { - if (cb == ' ' && end == j) { - continue; - } - break; - } - } - } - } - } - - if (j == end) { - break; - } - if (b == '*') { - // Glob match (only supported at the end) - return 0; - } - byte cb = s[j]; - int delta = b - cb; - if (delta != 0) { - cb = toLowerCase(cb); - b = toLowerCase(b); - delta = b - cb; - if (delta != 0) { - return delta; - } - } - - if (b == terminator || cb == terminator) { - return delta; - } - } - - return data[i] - terminator; - } - - /** - * Look up whether this word is a typo, and if so, return the typo itself - * and one or more likely meanings - * - * @param text the string containing the word - * @param begin the index of the first character in the word - * @param end the index of the first character after the word. Note that the - * search may extend <b>beyond</b> this index, if for example the - * word matches a multi-word typo in the dictionary - * @return a list of the typo itself followed by the replacement strings if - * the word represents a typo, and null otherwise - */ - @Nullable - public List<String> getTypos(@NonNull CharSequence text, int begin, int end) { - assert end <= text.length(); - - if (assertionsEnabled()) { - for (int i = begin; i < end; i++) { - char c = text.charAt(i); - if (c >= 128) { - assert false : "Call the UTF-8 version of this method instead"; - return null; - } - } - } - - int low = 0; - int high = mWordCount - 1; - while (low <= high) { - int middle = (low + high) >>> 1; - int offset = mIndices[middle]; - - if (DEBUG_SEARCH) { - System.out.println("Comparing string " + text +" with entry at " + offset - + ": " + dumpEntry(offset)); - } - - // Compare the word at the given index. - int compare = compare(mData, offset, (byte) 0, text, begin, end); - - if (compare == 0) { - offset = mIndices[middle]; - - // Don't allow matching uncapitalized words, such as "enlish", when - // the dictionary word is capitalized, "Enlish". - if (mData[offset] != text.charAt(begin) - && Character.isLowerCase(text.charAt(begin))) { - return null; - } - - // Make sure there is a case match; we only want to allow - // matching capitalized words to capitalized typos or uncapitalized typos - // (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized - // typos (e.g. "enlish" to "Enlish"). - String glob = null; - for (int i = begin; ; i++) { - byte b = mData[offset++]; - if (b == 0) { - offset--; - break; - } else if (b == '*') { - int globEnd = i; - while (globEnd < text.length() - && Character.isLetter(text.charAt(globEnd))) { - globEnd++; - } - glob = text.subSequence(i, globEnd).toString(); - break; - } - char c = text.charAt(i); - byte cb = (byte) c; - if (b != cb && i > begin) { - return null; - } - } - - return computeSuggestions(mIndices[middle], offset, glob); - } - - if (compare < 0) { - low = middle + 1; - } else if (compare > 0) { - high = middle - 1; - } else { - assert false; // compare == 0 already handled above - return null; - } - } - - return null; - } - - /** - * Look up whether this word is a typo, and if so, return the typo itself - * and one or more likely meanings - * - * @param utf8Text the string containing the word, encoded as UTF-8 - * @param begin the index of the first character in the word - * @param end the index of the first character after the word. Note that the - * search may extend <b>beyond</b> this index, if for example the - * word matches a multi-word typo in the dictionary - * @return a list of the typo itself followed by the replacement strings if - * the word represents a typo, and null otherwise - */ - @Nullable - public List<String> getTypos(@NonNull byte[] utf8Text, int begin, int end) { - assert end <= utf8Text.length; - - int low = 0; - int high = mWordCount - 1; - while (low <= high) { - int middle = (low + high) >>> 1; - int offset = mIndices[middle]; - - if (DEBUG_SEARCH) { - String s = new String(Arrays.copyOfRange(utf8Text, begin, end), Charsets.UTF_8); - System.out.println("Comparing string " + s +" with entry at " + offset - + ": " + dumpEntry(offset)); - System.out.println(" middle=" + middle + ", low=" + low + ", high=" + high); - } - - // Compare the word at the given index. - int compare = compare(mData, offset, (byte) 0, utf8Text, begin, end); - - if (DEBUG_SEARCH) { - System.out.println(" signum=" + (int)Math.signum(compare) + ", delta=" + compare); - } - - if (compare == 0) { - offset = mIndices[middle]; - - // Don't allow matching uncapitalized words, such as "enlish", when - // the dictionary word is capitalized, "Enlish". - if (mData[offset] != utf8Text[begin] && isUpperCase(mData[offset])) { - return null; - } - - // Make sure there is a case match; we only want to allow - // matching capitalized words to capitalized typos or uncapitalized typos - // (e.g. "Teh" and "teh" to "the"), but not uncapitalized words to capitalized - // typos (e.g. "enlish" to "Enlish"). - String glob = null; - for (int i = begin; ; i++) { - byte b = mData[offset++]; - if (b == 0) { - offset--; - break; - } else if (b == '*') { - int globEnd = i; - while (globEnd < utf8Text.length && isLetter(utf8Text[globEnd])) { - globEnd++; - } - glob = new String(utf8Text, i, globEnd - i, Charsets.UTF_8); - break; - } - byte cb = utf8Text[i]; - if (b != cb && i > begin) { - return null; - } - } - - return computeSuggestions(mIndices[middle], offset, glob); - } - - if (compare < 0) { - low = middle + 1; - } else if (compare > 0) { - high = middle - 1; - } else { - assert false; // compare == 0 already handled above - return null; - } - } - - return null; - } - - private List<String> computeSuggestions(int begin, int offset, String glob) { - String typo = new String(mData, begin, offset - begin, Charsets.UTF_8); - - if (glob != null) { - typo = typo.replaceAll("\\*", glob); //$NON-NLS-1$ - } - - assert mData[offset] == 0; - offset++; - int replacementEnd = offset; - while (mData[replacementEnd] != 0) { - replacementEnd++; - } - String replacements = new String(mData, offset, replacementEnd - offset, Charsets.UTF_8); - List<String> words = new ArrayList<String>(); - words.add(typo); - - // The first entry should be the typo itself. We need to pass this back since due - // to multi-match words and globbing it could extend beyond the initial word range - - for (String s : Splitter.on(',').omitEmptyStrings().trimResults().split(replacements)) { - if (glob != null) { - // Need to append the glob string to each result - words.add(s.replaceAll("\\*", glob)); //$NON-NLS-1$ - } else { - words.add(s); - } - } - - return words; - } - - // "Character" handling for bytes. This assumes that the bytes correspond to Unicode - // characters in the ISO 8859-1 range, which is are encoded the same way in UTF-8. - // This obviously won't work to for example uppercase to lowercase conversions for - // multi byte characters, which means we simply won't catch typos if the dictionaries - // contain these. None of the currently included dictionaries do. However, it does - // help us properly deal with punctuation and spacing characters. - - static boolean isUpperCase(byte b) { - return Character.isUpperCase((char) b); - } - - static byte toLowerCase(byte b) { - return (byte) Character.toLowerCase((char) b); - } - - static boolean isSpace(byte b) { - return Character.isWhitespace((char) b); - } - - static boolean isLetter(byte b) { - // Assume that multi byte characters represent letters in other languages. - // Obviously, it could be unusual punctuation etc but letters are more likely - // in this context. - return Character.isLetter((char) b) || (b & 0x80) != 0; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypographyDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypographyDetector.java deleted file mode 100644 index ae5f24c..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/TypographyDetector.java +++ /dev/null @@ -1,529 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.TAG_STRING; -import static com.android.SdkConstants.TAG_STRING_ARRAY; - -import com.android.annotations.NonNull; -import com.android.annotations.VisibleForTesting; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Checks for various typographical issues in string definitions. - */ -public class TypographyDetector extends ResourceXmlDetector { - /** Replace hyphens with dashes? */ - public static final Issue DASHES = Issue.create( - "TypographyDashes", //$NON-NLS-1$ - "Looks for usages of hyphens which can be replaced by n dash and m dash characters", - "The \"n dash\" (\u2013, –) and the \"m dash\" (\u2014, —) " + - "characters are used for ranges (n dash) and breaks (m dash). Using these " + - "instead of plain hyphens can make text easier to read and your application " + - "will look more polished.", - Category.TYPOGRAPHY, - 5, - Severity.WARNING, - TypographyDetector.class, - Scope.RESOURCE_FILE_SCOPE). - setMoreInfo("http://en.wikipedia.org/wiki/Dash"); //$NON-NLS-1$ - - /** Replace dumb quotes with smart quotes? */ - public static final Issue QUOTES = Issue.create( - "TypographyQuotes", //$NON-NLS-1$ - "Looks for straight quotes which can be replaced by curvy quotes", - "Straight single quotes and double quotes, when used as a pair, can be replaced " + - "by \"curvy quotes\" (or directional quotes). This can make the text more " + - "readable.\n" + - "\n" + - "Note that you should never use grave accents and apostrophes to quote, " + - "`like this'.\n" + - "\n" + - "(Also note that you should not use curvy quotes for code fragments.)", - Category.TYPOGRAPHY, - 5, - Severity.WARNING, - TypographyDetector.class, - Scope.RESOURCE_FILE_SCOPE). - setMoreInfo("http://en.wikipedia.org/wiki/Quotation_mark"). //$NON-NLS-1$ - // This feature is apparently controversial: recent apps have started using - // straight quotes to avoid inconsistencies. Disabled by default for now. - setEnabledByDefault(false); - - /** Replace fraction strings with fraction characters? */ - public static final Issue FRACTIONS = Issue.create( - "TypographyFractions", //$NON-NLS-1$ - "Looks for fraction strings which can be replaced with a fraction character", - "You can replace certain strings, such as 1/2, and 1/4, with dedicated " + - "characters for these, such as \u00BD (½) and \00BC (¼). " + - "This can help make the text more readable.", - Category.TYPOGRAPHY, - 5, - Severity.WARNING, - TypographyDetector.class, - Scope.RESOURCE_FILE_SCOPE). - setMoreInfo("http://en.wikipedia.org/wiki/Number_Forms"); //$NON-NLS-1$ - - /** Replace ... with the ellipsis character? */ - public static final Issue ELLIPSIS = Issue.create( - "TypographyEllipsis", //$NON-NLS-1$ - "Looks for ellipsis strings (...) which can be replaced with an ellipsis character", - "You can replace the string \"...\" with a dedicated ellipsis character, " + - "ellipsis character (\u2026, …). This can help make the text more readable.", - Category.TYPOGRAPHY, - 5, - Severity.WARNING, - TypographyDetector.class, - Scope.RESOURCE_FILE_SCOPE). - setMoreInfo("http://en.wikipedia.org/wiki/Ellipsis"); //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue OTHER = Issue.create( - "TypographyOther", //$NON-NLS-1$ - "Looks for miscellaneous typographical problems like replacing (c) with \u00A9", - "This check looks for miscellaneous typographical problems and offers replacement " + - "sequences that will make the text easier to read and your application more " + - "polished.", - Category.TYPOGRAPHY, - 3, - Severity.WARNING, - TypographyDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - private static final String GRAVE_QUOTE_MESSAGE = - "Avoid quoting with grave accents; use apostrophes or better yet directional quotes instead"; - private static final String ELLIPSIS_MESSAGE = - "Replace \"...\" with ellipsis character (\u2026, …) ?"; - private static final String EN_DASH_MESSAGE = - "Replace \"-\" with an \"en dash\" character (\u2013, –) ?"; - private static final String EM_DASH_MESSAGE = - "Replace \"--\" with an \"em dash\" character (\u2014, —) ?"; - private static final String TYPOGRAPHIC_APOSTROPHE_MESSAGE = - "Replace apostrophe (') with typographic apostrophe (\u2019, ’) ?"; - private static final String SINGLE_QUOTE_MESSAGE = - "Replace straight quotes ('') with directional quotes (\u2018\u2019, ‘ and ’) ?"; - private static final String DBL_QUOTES_MESSAGE = - "Replace straight quotes (\") with directional quotes (\u201C\u201D, “ and ”) ?"; - private static final String COPYRIGHT_MESSAGE = - "Replace (c) with copyright symbol \u00A9 (©) ?"; - - /** - * Pattern used to detect scenarios which can be replaced with n dashes: a - * numeric range with a hyphen in the middle (and possibly spaces) - */ - @VisibleForTesting - static final Pattern HYPHEN_RANGE_PATTERN = - Pattern.compile(".*(\\d+\\s*)-(\\s*\\d+).*"); //$NON-NLS-1$ - - /** - * Pattern used to detect scenarios where a grave accent mark is used - * to do ASCII quotations of the form `this'' or ``this'', which is frowned upon. - * This pattern tries to avoid falsely complaining about strings like - * "Type Option-` then 'Escape'." - */ - @VisibleForTesting - static final Pattern GRAVE_QUOTATION = - Pattern.compile("(^[^`]*`[^'`]+'[^']*$)|(^[^`]*``[^'`]+''[^']*$)"); //$NON-NLS-1$ - - /** - * Pattern used to detect common fractions, e.g. 1/2, 1/3, 2/3, 1/4, 3/4 and - * variations like 2 / 3, but not 11/22 and so on. - */ - @VisibleForTesting - static final Pattern FRACTION_PATTERN = - Pattern.compile(".*\\b([13])\\s*/\\s*([234])\\b.*"); //$NON-NLS-1$ - - /** - * Pattern used to detect single quote strings, such as 'hello', but - * not just quoted strings like 'Double quote: "', and not sentences - * where there are multiple apostrophes but not in a quoting context such - * as "Mind Your P's and Q's". - */ - @VisibleForTesting - static final Pattern SINGLE_QUOTE = - Pattern.compile(".*\\W*'[^']+'(\\W.*)?"); //$NON-NLS-1$ - - private static final String FRACTION_MESSAGE = - "Use fraction character %1$c (%2$s) instead of %3$s ?"; - - private static final String FRACTION_MESSAGE_PATTERN = - "Use fraction character (.+) \\((.+)\\) instead of (.+) \\?"; - - private boolean mCheckDashes; - private boolean mCheckQuotes; - private boolean mCheckFractions; - private boolean mCheckEllipsis; - private boolean mCheckMisc; - - /** Constructs a new {@link TypographyDetector} */ - public TypographyDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.VALUES; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_STRING, - TAG_STRING_ARRAY - ); - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - mCheckDashes = context.isEnabled(DASHES); - mCheckQuotes = context.isEnabled(QUOTES); - mCheckFractions = context.isEnabled(FRACTIONS); - mCheckEllipsis = context.isEnabled(ELLIPSIS); - mCheckMisc = context.isEnabled(OTHER); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - NodeList childNodes = element.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - String text = child.getNodeValue(); - checkText(context, element, child, text); - } else if (child.getNodeType() == Node.ELEMENT_NODE && - child.getParentNode().getNodeName().equals(TAG_STRING_ARRAY)) { - // String array item children - NodeList items = child.getChildNodes(); - for (int j = 0, m = items.getLength(); j < m; j++) { - Node item = items.item(j); - if (item.getNodeType() == Node.TEXT_NODE) { - String text = item.getNodeValue(); - checkText(context, child, item, text); - } - } - } - } - } - - private void checkText(XmlContext context, Node element, Node textNode, String text) { - if (mCheckEllipsis) { - // Replace ... with ellipsis character? - int ellipsis = text.indexOf("..."); //$NON-NLS-1$ - if (ellipsis != -1 && !text.startsWith(".", ellipsis + 3)) { //$NON-NLS-1$ - context.report(ELLIPSIS, element, context.getLocation(textNode), - ELLIPSIS_MESSAGE, null); - } - } - - // Dashes - if (mCheckDashes) { - int hyphen = text.indexOf('-'); - if (hyphen != -1) { - // n dash - Matcher matcher = HYPHEN_RANGE_PATTERN.matcher(text); - if (matcher.matches()) { - // Make sure that if there is no space before digit there isn't - // one on the left either -- since we don't want to consider - // "1 2 -3" as a range from 2 to 3 - boolean isNegativeNumber = - !Character.isWhitespace(matcher.group(2).charAt(0)) && - Character.isWhitespace(matcher.group(1).charAt( - matcher.group(1).length() - 1)); - if (!isNegativeNumber && !isAnalyticsTrackingId((Element) element)) { - context.report(DASHES, element, context.getLocation(textNode), - EN_DASH_MESSAGE, - null); - } - } - - // m dash - int emdash = text.indexOf("--"); //$NON-NLS-1$ - // Don't suggest replacing -- or "--" with an m dash since these are sometimes - // used as digit marker strings - if (emdash > 1 && !text.startsWith("-", emdash + 2)) { //$NON-NLS-1$ - context.report(DASHES, element, context.getLocation(textNode), - EM_DASH_MESSAGE, null); - } - } - } - - if (mCheckQuotes) { - // Check for single quotes that can be replaced with directional quotes - int quoteStart = text.indexOf('\''); - if (quoteStart != -1) { - int quoteEnd = text.indexOf('\'', quoteStart + 1); - if (quoteEnd != -1 && quoteEnd > quoteStart + 1 - && (quoteEnd < text.length() -1 || quoteStart > 0) - && SINGLE_QUOTE.matcher(text).matches()) { - context.report(QUOTES, element, context.getLocation(textNode), - SINGLE_QUOTE_MESSAGE, null); - return; - } - - // Check for apostrophes that can be replaced by typographic apostrophes - if (quoteEnd == -1 && quoteStart > 0 - && Character.isLetterOrDigit(text.charAt(quoteStart - 1))) { - context.report(QUOTES, element, context.getLocation(textNode), - TYPOGRAPHIC_APOSTROPHE_MESSAGE, null); - return; - } - } - - // Check for double quotes that can be replaced by directional double quotes - quoteStart = text.indexOf('"'); - if (quoteStart != -1) { - int quoteEnd = text.indexOf('"', quoteStart + 1); - if (quoteEnd != -1 && quoteEnd > quoteStart + 1) { - if (quoteEnd < text.length() -1 || quoteStart > 0) { - context.report(QUOTES, element, context.getLocation(textNode), - DBL_QUOTES_MESSAGE, null); - return; - } - } - } - - // Check for grave accent quotations - if (text.indexOf('`') != -1 && GRAVE_QUOTATION.matcher(text).matches()) { - // Are we indenting ``like this'' or `this' ? If so, complain - context.report(QUOTES, element, context.getLocation(textNode), - GRAVE_QUOTE_MESSAGE, null); - return; - } - - // Consider suggesting other types of directional quotes, such as guillemets, in - // other languages? - // There are a lot of exceptions and special cases to be considered so - // this will need careful implementation and testing. - // See http://en.wikipedia.org/wiki/Non-English_usage_of_quotation_marks - } - - // Fraction symbols? - if (mCheckFractions && text.indexOf('/') != -1) { - Matcher matcher = FRACTION_PATTERN.matcher(text); - if (matcher.matches()) { - String top = matcher.group(1); // Numerator - String bottom = matcher.group(2); // Denominator - if (top.equals("1") && bottom.equals("2")) { //$NON-NLS-1$ //$NON-NLS-2$ - context.report(FRACTIONS, element, context.getLocation(textNode), - String.format(FRACTION_MESSAGE, '\u00BD', "½", "1/2"), null); - } else if (top.equals("1") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$ - context.report(FRACTIONS, element, context.getLocation(textNode), - String.format(FRACTION_MESSAGE, '\u00BC', "¼", "1/4"), null); - } else if (top.equals("3") && bottom.equals("4")) { //$NON-NLS-1$ //$NON-NLS-2$ - context.report(FRACTIONS, element, context.getLocation(textNode), - String.format(FRACTION_MESSAGE, '\u00BE', "¾", "3/4"), null); - } else if (top.equals("1") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$ - context.report(FRACTIONS, element, context.getLocation(textNode), - String.format(FRACTION_MESSAGE, '\u2153', "⅓", "1/3"), null); - } else if (top.equals("2") && bottom.equals("3")) { //$NON-NLS-1$ //$NON-NLS-2$ - context.report(FRACTIONS, element, context.getLocation(textNode), - String.format(FRACTION_MESSAGE, '\u2154', "⅔", "2/3"), null); - } - } - } - - if (mCheckMisc) { - // Fix copyright symbol? - if (text.indexOf('(') != -1 - && (text.contains("(c)") || text.contains("(C)"))) { //$NON-NLS-1$ //$NON-NLS-2$ - // Suggest replacing with copyright symbol? - context.report(OTHER, element, context.getLocation(textNode), - COPYRIGHT_MESSAGE, null); - // Replace (R) and TM as well? There are unicode characters for these but they - // are probably not very common within Android app strings. - } - } - } - - private static boolean isAnalyticsTrackingId(Element element) { - String name = element.getAttribute(ATTR_NAME); - return "ga_trackingId".equals(name); //$NON-NLS-1$ - } - - /** - * An object describing a single edit to be made. The offset points to a - * location to start editing; the length is the number of characters to - * delete, and the replaceWith string points to a string to insert at the - * offset. Note that this can model not just replacement edits but deletions - * (empty replaceWith) and insertions (replace length = 0) too. - */ - public static class ReplaceEdit { - /** The offset of the edit */ - public final int offset; - /** The number of characters to delete at the offset */ - public final int length; - /** The characters to insert at the offset */ - public final String replaceWith; - - /** - * Creates a new replace edit - * - * @param offset the offset of the edit - * @param length the number of characters to delete at the offset - * @param replaceWith the characters to insert at the offset - */ - public ReplaceEdit(int offset, int length, String replaceWith) { - super(); - this.offset = offset; - this.length = length; - this.replaceWith = replaceWith; - } - } - - /** - * Returns a list of edits to be applied to fix the suggestion made by the - * given warning. The specific issue id and message should be the message - * provided by this detector in an earlier run. - * <p> - * This is intended to help tools implement automatic fixes of these - * warnings. The reason only the message and issue id can be provided - * instead of actual state passed in the data field to a reporter is that - * fix operation can be run much later than the lint is processed (for - * example, in a subsequent run of the IDE when only the warnings have been - * persisted), - * - * @param issueId the issue id, which should be the id for one of the - * typography issues - * @param message the actual error message, which should be a message - * provided by this detector - * @param textNode a text node which corresponds to the text node the - * warning operated on - * @return a list of edits, which is never null but could be empty. The - * offsets in the edit objects are relative to the text node. - */ - public static List<ReplaceEdit> getEdits(String issueId, String message, Node textNode) { - return getEdits(issueId, message, textNode.getNodeValue()); - } - - /** - * Returns a list of edits to be applied to fix the suggestion made by the - * given warning. The specific issue id and message should be the message - * provided by this detector in an earlier run. - * <p> - * This is intended to help tools implement automatic fixes of these - * warnings. The reason only the message and issue id can be provided - * instead of actual state passed in the data field to a reporter is that - * fix operation can be run much later than the lint is processed (for - * example, in a subsequent run of the IDE when only the warnings have been - * persisted), - * - * @param issueId the issue id, which should be the id for one of the - * typography issues - * @param message the actual error message, which should be a message - * provided by this detector - * @param text the text of the XML node where the warning appeared - * @return a list of edits, which is never null but could be empty. The - * offsets in the edit objects are relative to the text node. - */ - public static List<ReplaceEdit> getEdits(String issueId, String message, String text) { - List<ReplaceEdit> edits = new ArrayList<ReplaceEdit>(); - if (message.equals(ELLIPSIS_MESSAGE)) { - int offset = text.indexOf("..."); //$NON-NLS-1$ - if (offset != -1) { - edits.add(new ReplaceEdit(offset, 3, "\u2026")); //$NON-NLS-1$ - } - } else if (message.equals(EN_DASH_MESSAGE)) { - int offset = text.indexOf('-'); - if (offset != -1) { - edits.add(new ReplaceEdit(offset, 1, "\u2013")); //$NON-NLS-1$ - } - } else if (message.equals(EM_DASH_MESSAGE)) { - int offset = text.indexOf("--"); //$NON-NLS-1$ - if (offset != -1) { - edits.add(new ReplaceEdit(offset, 2, "\u2014")); //$NON-NLS-1$ - } - } else if (message.equals(TYPOGRAPHIC_APOSTROPHE_MESSAGE)) { - int offset = text.indexOf('\''); - if (offset != -1) { - edits.add(new ReplaceEdit(offset, 1, "\u2019")); //$NON-NLS-1$ - } - } else if (message.equals(COPYRIGHT_MESSAGE)) { - int offset = text.indexOf("(c)"); //$NON-NLS-1$ - if (offset == -1) { - offset = text.indexOf("(C)"); //$NON-NLS-1$ - } - if (offset != -1) { - edits.add(new ReplaceEdit(offset, 3, "\u00A9")); //$NON-NLS-1$ - } - } else if (message.equals(SINGLE_QUOTE_MESSAGE)) { - int offset = text.indexOf('\''); - if (offset != -1) { - int endOffset = text.indexOf('\'', offset + 1); //$NON-NLS-1$ - if (endOffset != -1) { - edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$ - edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$ - } - } - } else if (message.equals(DBL_QUOTES_MESSAGE)) { - int offset = text.indexOf('"'); - if (offset != -1) { - int endOffset = text.indexOf('"', offset + 1); - if (endOffset != -1) { - edits.add(new ReplaceEdit(offset, 1, "\u201C")); //$NON-NLS-1$ - edits.add(new ReplaceEdit(endOffset, 1, "\u201D")); //$NON-NLS-1$ - } - } - } else if (message.equals(GRAVE_QUOTE_MESSAGE)) { - int offset = text.indexOf('`'); - if (offset != -1) { - int endOffset = text.indexOf('\'', offset + 1); - if (endOffset != -1) { - edits.add(new ReplaceEdit(offset, 1, "\u2018")); //$NON-NLS-1$ - edits.add(new ReplaceEdit(endOffset, 1, "\u2019")); //$NON-NLS-1$ - } - } - } else { - Matcher matcher = Pattern.compile(FRACTION_MESSAGE_PATTERN).matcher(message); - if (matcher.find()) { - // "Use fraction character %1$c (%2$s) instead of %3$s ?"; - String replace = matcher.group(3); - int offset = text.indexOf(replace); - if (offset != -1) { - String replaceWith = matcher.group(2); - edits.add(new ReplaceEdit(offset, replace.length(), replaceWith)); - } - } - } - - return edits; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java deleted file mode 100644 index caa112a..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UnusedResourceDetector.java +++ /dev/null @@ -1,594 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_REF_PREFIX; -import static com.android.SdkConstants.DOT_GIF; -import static com.android.SdkConstants.DOT_JPG; -import static com.android.SdkConstants.DOT_PNG; -import static com.android.SdkConstants.DOT_XML; -import static com.android.SdkConstants.RESOURCE_CLR_STYLEABLE; -import static com.android.SdkConstants.RESOURCE_CLZ_ARRAY; -import static com.android.SdkConstants.RESOURCE_CLZ_ID; -import static com.android.SdkConstants.R_ATTR_PREFIX; -import static com.android.SdkConstants.R_CLASS; -import static com.android.SdkConstants.R_ID_PREFIX; -import static com.android.SdkConstants.R_PREFIX; -import static com.android.SdkConstants.TAG_ARRAY; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.TAG_PLURALS; -import static com.android.SdkConstants.TAG_RESOURCES; -import static com.android.SdkConstants.TAG_STRING_ARRAY; -import static com.android.SdkConstants.TAG_STYLE; -import static com.android.tools.lint.detector.api.LintUtils.endsWith; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceType; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Project; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.google.common.collect.Lists; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import lombok.ast.AstVisitor; -import lombok.ast.ClassDeclaration; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.NormalTypeBody; -import lombok.ast.VariableDeclaration; -import lombok.ast.VariableDefinition; - -/** - * Finds unused resources. - * <p> - * Note: This detector currently performs *string* analysis to check Java files. - * The Lint API needs an official Java AST API (or map to an existing one like - * BCEL for bytecode analysis etc) and once it does this should be updated to - * use it. - */ -public class UnusedResourceDetector extends ResourceXmlDetector implements Detector.JavaScanner { - - /** Unused resources (other than ids). */ - public static final Issue ISSUE = Issue.create("UnusedResources", //$NON-NLS-1$ - "Looks for unused resources", - "Unused resources make applications larger and slow down builds.", - Category.PERFORMANCE, - 3, - Severity.WARNING, - UnusedResourceDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES)); - - /** Unused id's */ - public static final Issue ISSUE_IDS = Issue.create("UnusedIds", //$NON-NLS-1$ - "Looks for unused id's", - "This resource id definition appears not to be needed since it is not referenced " + - "from anywhere. Having id definitions, even if unused, is not necessarily a bad " + - "idea since they make working on layouts and menus easier, so there is not a " + - "strong reason to delete these.", - Category.PERFORMANCE, - 1, - Severity.WARNING, - UnusedResourceDetector.class, - EnumSet.of(Scope.MANIFEST, Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES)) - .setEnabledByDefault(false); - - private Set<String> mDeclarations; - private Set<String> mReferences; - private Map<String, Location> mUnused; - - /** - * Constructs a new {@link UnusedResourceDetector} - */ - public UnusedResourceDetector() { - } - - @Override - public void run(@NonNull Context context) { - assert false; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - return true; - } - - @Override - public void beforeCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mDeclarations = new HashSet<String>(300); - mReferences = new HashSet<String>(300); - } - } - - // ---- Implements JavaScanner ---- - - @Override - public void beforeCheckFile(@NonNull Context context) { - File file = context.file; - - String fileName = file.getName(); - boolean isXmlFile = endsWith(fileName, DOT_XML); - if (isXmlFile - || endsWith(fileName, DOT_PNG) - || endsWith(fileName, DOT_JPG) - || endsWith(fileName, DOT_GIF)) { - String parentName = file.getParentFile().getName(); - int dash = parentName.indexOf('-'); - String typeName = parentName.substring(0, dash == -1 ? parentName.length() : dash); - ResourceType type = ResourceType.getEnum(typeName); - if (type != null && LintUtils.isFileBasedResourceType(type)) { - String baseName = fileName.substring(0, fileName.length() - DOT_XML.length()); - String resource = R_PREFIX + typeName + '.' + baseName; - if (context.getPhase() == 1) { - mDeclarations.add(resource); - } else { - assert context.getPhase() == 2; - if (mUnused.containsKey(resource)) { - // Check whether this is an XML document that has a tools:ignore attribute - // on the document element: if so don't record it as a declaration. - if (isXmlFile && context instanceof XmlContext) { - XmlContext xmlContext = (XmlContext) context; - if (xmlContext.document != null - && xmlContext.document.getDocumentElement() != null) { - Element root = xmlContext.document.getDocumentElement(); - if (xmlContext.getDriver().isSuppressed(ISSUE, root)) { - // Also remove it from consideration such that even the - // presence of this field in the R file is ignored. - mUnused.remove(resource); - return; - } - } - } - - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - mUnused.remove(resource); - return; - } - - recordLocation(resource, Location.create(file)); - } - } - } - } - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (context.getPhase() == 1) { - mDeclarations.removeAll(mReferences); - Set<String> unused = mDeclarations; - mReferences = null; - mDeclarations = null; - - // Remove styles and attributes: they may be used, analysis isn't complete for these - List<String> styles = new ArrayList<String>(); - for (String resource : unused) { - // R.style.x, R.styleable.x, R.attr - if (resource.startsWith("R.style") //$NON-NLS-1$ - || resource.startsWith("R.attr")) { //$NON-NLS-1$ - styles.add(resource); - } - } - unused.removeAll(styles); - - // Remove id's if the user has disabled reporting issue ids - if (!unused.isEmpty() && !context.isEnabled(ISSUE_IDS)) { - // Remove all R.id references - List<String> ids = new ArrayList<String>(); - for (String resource : unused) { - if (resource.startsWith(R_ID_PREFIX)) { - ids.add(resource); - } - } - unused.removeAll(ids); - } - - if (!unused.isEmpty() && !context.getDriver().hasParserErrors()) { - mUnused = new HashMap<String, Location>(unused.size()); - for (String resource : unused) { - mUnused.put(resource, null); - } - - // Request another pass, and in the second pass we'll gather location - // information for all declaration locations we've found - context.requestRepeat(this, Scope.ALL_RESOURCES_SCOPE); - } - } else { - assert context.getPhase() == 2; - - // Report any resources that we (for some reason) could not find a declaration - // location for - if (!mUnused.isEmpty()) { - // Fill in locations for files that we didn't encounter in other ways - for (Map.Entry<String, Location> entry : mUnused.entrySet()) { - String resource = entry.getKey(); - Location location = entry.getValue(); - if (location != null) { - continue; - } - - // Try to figure out the file if it's a file based resource (such as R.layout) -- - // in that case we can figure out the filename since it has a simple mapping - // from the resource name (though the presence of qualifiers like -land etc - // makes it a little tricky if there's no base file provided) - int secondDot = resource.indexOf('.', 2); - String typeName = resource.substring(2, secondDot); // 2: Skip R. - ResourceType type = ResourceType.getEnum(typeName); - if (type != null && LintUtils.isFileBasedResourceType(type)) { - String name = resource.substring(secondDot + 1); - - List<File> folders = Lists.newArrayList(); - List<File> resourceFolders = context.getProject().getResourceFolders(); - for (File res : resourceFolders) { - File[] f = res.listFiles(); - if (f != null) { - folders.addAll(Arrays.asList(f)); - } - } - if (folders != null) { - // Process folders in alphabetical order such that we process - // based folders first: we want the locations in base folder - // order - Collections.sort(folders, new Comparator<File>() { - @Override - public int compare(File file1, File file2) { - return file1.getName().compareTo(file2.getName()); - } - }); - for (File folder : folders) { - if (folder.getName().startsWith(typeName)) { - File[] files = folder.listFiles(); - if (files != null) { - for (File file : files) { - String fileName = file.getName(); - if (fileName.startsWith(name) - && fileName.startsWith(".", //$NON-NLS-1$ - name.length())) { - recordLocation(resource, Location.create(file)); - } - } - } - } - } - } - } - } - - List<String> sorted = new ArrayList<String>(mUnused.keySet()); - Collections.sort(sorted); - - Boolean skippedLibraries = null; - - for (String resource : sorted) { - Location location = mUnused.get(resource); - if (location != null) { - // We were prepending locations, but we want to prefer the base folders - location = Location.reverse(location); - } - - if (location == null) { - if (skippedLibraries == null) { - skippedLibraries = false; - for (Project project : context.getDriver().getProjects()) { - if (!project.getReportIssues()) { - skippedLibraries = true; - break; - } - } - } - if (skippedLibraries) { - // Skip this resource if we don't have a location, and one or - // more library projects were skipped; the resource was very - // probably defined in that library project and only encountered - // in the main project's java R file - continue; - } - } - - String message = String.format("The resource %1$s appears to be unused", - resource); - Issue issue = getIssue(resource); - // TODO: Compute applicable node scope - context.report(issue, location, message, resource); - } - } - } - } - - private static Issue getIssue(String resource) { - return resource.startsWith(R_ID_PREFIX) ? ISSUE_IDS : ISSUE; - } - - private void recordLocation(String resource, Location location) { - Location oldLocation = mUnused.get(resource); - if (oldLocation != null) { - location.setSecondary(oldLocation); - } - mUnused.put(resource, location); - } - - @Override - public Collection<String> getApplicableAttributes() { - return ALL; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - TAG_STYLE, - TAG_RESOURCES, - TAG_ARRAY, - TAG_STRING_ARRAY, - TAG_PLURALS - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (TAG_RESOURCES.equals(element.getTagName())) { - for (Element item : LintUtils.getChildren(element)) { - Attr nameAttribute = item.getAttributeNode(ATTR_NAME); - if (nameAttribute != null) { - String name = nameAttribute.getValue(); - if (name.indexOf('.') != -1) { - name = name.replace('.', '_'); - } - String type = item.getTagName(); - if (type.equals(TAG_ITEM)) { - type = RESOURCE_CLZ_ID; - } else if (type.equals("declare-styleable")) { //$NON-NLS-1$ - type = RESOURCE_CLR_STYLEABLE; - } else if (type.contains("array")) { //$NON-NLS-1$ - // <string-array> etc - type = RESOURCE_CLZ_ARRAY; - } - String resource = R_PREFIX + type + '.' + name; - - if (context.getPhase() == 1) { - mDeclarations.add(resource); - checkChildRefs(item); - } else { - assert context.getPhase() == 2; - if (mUnused.containsKey(resource)) { - if (context.getDriver().isSuppressed(getIssue(resource), item)) { - mUnused.remove(resource); - continue; - } - if (!context.getProject().getReportIssues()) { - mUnused.remove(resource); - continue; - } - if (isAnalyticsFile(context)) { - mUnused.remove(resource); - continue; - } - - recordLocation(resource, context.getLocation(nameAttribute)); - } - } - } - } - } else if (mReferences != null) { - assert TAG_STYLE.equals(element.getTagName()) - || TAG_ARRAY.equals(element.getTagName()) - || TAG_PLURALS.equals(element.getTagName()) - || TAG_STRING_ARRAY.equals(element.getTagName()); - for (Element item : LintUtils.getChildren(element)) { - checkChildRefs(item); - } - } - } - - private static final String ANALYTICS_FILE = "analytics.xml"; //$NON-NLS-1$ - - /** - * Returns true if this XML file corresponds to an Analytics configuration file; - * these contain some attributes read by the library which won't be flagged as - * used by the application - * - * @param context the context used for scanning - * @return true if the file represents an analytics file - */ - public static boolean isAnalyticsFile(Context context) { - File file = context.file; - return file.getPath().endsWith(ANALYTICS_FILE) && file.getName().equals(ANALYTICS_FILE); - } - - private void checkChildRefs(Element item) { - // Look for ?attr/ and @dimen/foo etc references in the item children - NodeList childNodes = item.getChildNodes(); - for (int i = 0, n = childNodes.getLength(); i < n; i++) { - Node child = childNodes.item(i); - if (child.getNodeType() == Node.TEXT_NODE) { - String text = child.getNodeValue(); - - int index = text.indexOf(ATTR_REF_PREFIX); - if (index != -1) { - String name = text.substring(index + ATTR_REF_PREFIX.length()).trim(); - mReferences.add(R_ATTR_PREFIX + name); - } else { - index = text.indexOf('@'); - if (index != -1 && text.indexOf('/', index) != -1 - && !text.startsWith("@android:", index)) { //$NON-NLS-1$ - // Compute R-string, e.g. @string/foo => R.string.foo - String token = text.substring(index + 1).trim().replace('/', '.'); - String r = R_PREFIX + token; - mReferences.add(r); - } - } - } - } - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String value = attribute.getValue(); - - if (value.startsWith("@+") && !value.startsWith("@+android")) { //$NON-NLS-1$ //$NON-NLS-2$ - String resource = R_PREFIX + value.substring(2).replace('/', '.'); - // We already have the declarations when we scan the R file, but we're tracking - // these here to get attributes for position info - - if (context.getPhase() == 1) { - mDeclarations.add(resource); - } else if (mUnused.containsKey(resource)) { - if (context.getDriver().isSuppressed(getIssue(resource), attribute)) { - mUnused.remove(resource); - return; - } - if (!context.getProject().getReportIssues()) { - mUnused.remove(resource); - return; - } - recordLocation(resource, context.getLocation(attribute)); - return; - } - } else if (mReferences != null) { - if (value.startsWith("@") //$NON-NLS-1$ - && !value.startsWith("@android:")) { //$NON-NLS-1$ - // Compute R-string, e.g. @string/foo => R.string.foo - String r = R_PREFIX + value.substring(1).replace('/', '.'); - mReferences.add(r); - } else if (value.startsWith(ATTR_REF_PREFIX)) { - mReferences.add(R_ATTR_PREFIX + value.substring(ATTR_REF_PREFIX.length())); - } - } - - if (attribute.getNamespaceURI() != null - && !ANDROID_URI.equals(attribute.getNamespaceURI()) && mReferences != null) { - mReferences.add(R_ATTR_PREFIX + attribute.getLocalName()); - } - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public List<Class<? extends lombok.ast.Node>> getApplicableNodeTypes() { - return Collections.<Class<? extends lombok.ast.Node>>singletonList(ClassDeclaration.class); - } - - @Override - public boolean appliesToResourceRefs() { - return true; - } - - @Override - public void visitResourceReference(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull lombok.ast.Node node, @NonNull String type, @NonNull String name, - boolean isFramework) { - if (mReferences != null && !isFramework) { - String reference = R_PREFIX + type + '.' + name; - mReferences.add(reference); - } - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - if (mReferences != null) { - return new UnusedResourceVisitor(); - } else { - // Second pass, computing resource declaration locations: No need to look at Java - return null; - } - } - - // Look for references and declarations - private class UnusedResourceVisitor extends ForwardingAstVisitor { - @Override - public boolean visitClassDeclaration(ClassDeclaration node) { - // Look for declarations of R class fields and store them in - // mDeclarations - String description = node.getDescription(); - if (description.equals(R_CLASS)) { - // This is an R class. We can process this class very deliberately. - // The R class has a very specific AST format: - // ClassDeclaration ("R") - // NormalTypeBody - // ClassDeclaration (e.g. "drawable") - // NormalTypeBody - // VariableDeclaration - // VariableDefinition (e.g. "ic_launcher") - for (lombok.ast.Node body : node.getChildren()) { - if (body instanceof NormalTypeBody) { - for (lombok.ast.Node subclass : body.getChildren()) { - if (subclass instanceof ClassDeclaration) { - String className = ((ClassDeclaration) subclass).getDescription(); - for (lombok.ast.Node innerBody : subclass.getChildren()) { - if (innerBody instanceof NormalTypeBody) { - for (lombok.ast.Node field : innerBody.getChildren()) { - if (field instanceof VariableDeclaration) { - for (lombok.ast.Node child : field.getChildren()) { - if (child instanceof VariableDefinition) { - VariableDefinition def = - (VariableDefinition) child; - String name = def.astVariables().first() - .astName().astValue(); - String resource = R_PREFIX + className - + '.' + name; - mDeclarations.add(resource); - } // Else: It could be a comment node - } - } - } - } - } - } - } - } - } - - return true; - } - - return false; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UseCompoundDrawableDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UseCompoundDrawableDetector.java deleted file mode 100644 index db5de4d..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UseCompoundDrawableDetector.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BACKGROUND; -import static com.android.SdkConstants.ATTR_LAYOUT_WEIGHT; -import static com.android.SdkConstants.ATTR_SCALE_TYPE; -import static com.android.SdkConstants.IMAGE_VIEW; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.TEXT_VIEW; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -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.XmlContext; - -import org.w3c.dom.Element; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -/** - * Checks whether the current node can be replaced by a TextView using compound - * drawables. - */ -public class UseCompoundDrawableDetector extends LayoutDetector { - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "UseCompoundDrawables", //$NON-NLS-1$ - "Checks whether the current node can be replaced by a TextView using compound drawables.", - "A `LinearLayout` which contains an `ImageView` and a `TextView` can be more " + - "efficiently handled as a compound drawable (a single TextView, using the " + - "`drawableTop`, `drawableLeft`, `drawableRight` and/or `drawableBottom` attributes " + - "to draw one or more images adjacent to the text).\n" + - "\n" + - "If the two widgets are offset from each other with " + - "margins, this can be replaced with a `drawablePadding` attribute.\n" + - "\n" + - "There's a lint quickfix to perform this conversion in the Eclipse plugin.", - Category.PERFORMANCE, - 6, - Severity.WARNING, - UseCompoundDrawableDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link UseCompoundDrawableDetector} */ - public UseCompoundDrawableDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Collections.singletonList( - LINEAR_LAYOUT - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - int childCount = LintUtils.getChildCount(element); - if (childCount == 2) { - List<Element> children = LintUtils.getChildren(element); - Element first = children.get(0); - Element second = children.get(1); - if ((first.getTagName().equals(IMAGE_VIEW) && - second.getTagName().equals(TEXT_VIEW) && - !first.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)) || - ((second.getTagName().equals(IMAGE_VIEW) && - first.getTagName().equals(TEXT_VIEW) && - !second.hasAttributeNS(ANDROID_URI, ATTR_LAYOUT_WEIGHT)))) { - // If the layout has a background, ignore since it would disappear from - // the TextView - if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { - return; - } - - // Certain scale types cannot be done with compound drawables - String scaleType = first.getTagName().equals(IMAGE_VIEW) - ? first.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE) - : second.getAttributeNS(ANDROID_URI, ATTR_SCALE_TYPE); - if (scaleType != null && !scaleType.isEmpty()) { - // For now, ignore if any scale type is set - return; - } - - context.report(ISSUE, element, context.getLocation(element), - "This tag and its children can be replaced by one <TextView/> and " + - "a compound drawable", null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UselessViewDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UselessViewDetector.java deleted file mode 100644 index 867c3c4..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/UselessViewDetector.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ABSOLUTE_LAYOUT; -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_BACKGROUND; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_STYLE; -import static com.android.SdkConstants.FRAME_LAYOUT; -import static com.android.SdkConstants.GRID_LAYOUT; -import static com.android.SdkConstants.GRID_VIEW; -import static com.android.SdkConstants.HORIZONTAL_SCROLL_VIEW; -import static com.android.SdkConstants.LINEAR_LAYOUT; -import static com.android.SdkConstants.VIEW_MERGE; -import static com.android.SdkConstants.RADIO_GROUP; -import static com.android.SdkConstants.RELATIVE_LAYOUT; -import static com.android.SdkConstants.SCROLL_VIEW; -import static com.android.SdkConstants.TABLE_LAYOUT; -import static com.android.SdkConstants.TABLE_ROW; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -/** - * Checks whether the current node can be removed without affecting the layout. - */ -public class UselessViewDetector extends LayoutDetector { - /** Issue of including a parent that has no value on its own */ - public static final Issue USELESS_PARENT = Issue.create( - "UselessParent", //$NON-NLS-1$ - "Checks whether a parent layout can be removed.", - "A layout with children that has no siblings, is not a scrollview or " + - "a root layout, and does not have a background, can be removed and have " + - "its children moved directly into the parent for a flatter and more " + - "efficient layout hierarchy.", - Category.PERFORMANCE, - 2, - Severity.WARNING, - UselessViewDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Issue of including a leaf that isn't shown */ - public static final Issue USELESS_LEAF = Issue.create( - "UselessLeaf", //$NON-NLS-1$ - "Checks whether a leaf layout can be removed.", - "A layout that has no children or no background can often be removed (since it " + - "is invisible) for a flatter and more efficient layout hierarchy.", - Category.PERFORMANCE, - 2, - Severity.WARNING, - UselessViewDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link UselessViewDetector} */ - public UselessViewDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - private static final List<String> CONTAINERS = new ArrayList<String>(18); - static { - CONTAINERS.add(ABSOLUTE_LAYOUT); - CONTAINERS.add(FRAME_LAYOUT); - CONTAINERS.add(GRID_LAYOUT); - CONTAINERS.add(GRID_VIEW); - CONTAINERS.add(HORIZONTAL_SCROLL_VIEW); - CONTAINERS.add("ImageSwitcher"); //$NON-NLS-1$ - CONTAINERS.add(LINEAR_LAYOUT); - CONTAINERS.add(RADIO_GROUP); - CONTAINERS.add(RELATIVE_LAYOUT); - CONTAINERS.add(SCROLL_VIEW); - CONTAINERS.add("SlidingDrawer"); //$NON-NLS-1$ - CONTAINERS.add("StackView"); //$NON-NLS-1$ - CONTAINERS.add(TABLE_LAYOUT); - CONTAINERS.add(TABLE_ROW); - CONTAINERS.add("TextSwitcher"); //$NON-NLS-1$ - CONTAINERS.add("ViewAnimator"); //$NON-NLS-1$ - CONTAINERS.add("ViewFlipper"); //$NON-NLS-1$ - CONTAINERS.add("ViewSwitcher"); //$NON-NLS-1$ - // Available ViewGroups that are not included by this check: - // CONTAINERS.add("android.gesture.GestureOverlayView"); - // CONTAINERS.add("AdapterViewFlipper"); - // CONTAINERS.add("DialerFilter"); - // CONTAINERS.add("ExpandableListView"); - // CONTAINERS.add("ListView"); - // CONTAINERS.add("MediaController"); - // CONTAINERS.add("merge"); - // CONTAINERS.add("SearchView"); - // CONTAINERS.add("TabWidget"); - // CONTAINERS.add("TabHost"); - } - @Override - public Collection<String> getApplicableElements() { - return CONTAINERS; - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - int childCount = LintUtils.getChildCount(element); - if (childCount == 0) { - // Check to see if this is a leaf layout that can be removed - checkUselessLeaf(context, element); - } else { - // Check to see if this is a middle-man layout which can be removed - checkUselessMiddleLayout(context, element); - } - } - - // This is the old UselessLayoutCheck from layoutopt - private static void checkUselessMiddleLayout(XmlContext context, Element element) { - // Conditions: - // - The node has children - // - The node does not have siblings - // - The node's parent is not a scroll view (horizontal or vertical) - // - The node does not have a background or its parent does not have a - // background or neither the node and its parent have a background - // - The parent is not a <merge/> - - Node parentNode = element.getParentNode(); - if (parentNode.getNodeType() != Node.ELEMENT_NODE) { - // Can't remove root - return; - } - - Element parent = (Element) parentNode; - String parentTag = parent.getTagName(); - if (parentTag.equals(SCROLL_VIEW) || parentTag.equals(HORIZONTAL_SCROLL_VIEW) || - parentTag.equals(VIEW_MERGE)) { - // Can't remove if the parent is a scroll view or a merge - return; - } - - // This method is only called when we've already ensured that it has children - assert LintUtils.getChildCount(element) > 0; - - int parentChildCount = LintUtils.getChildCount(parent); - if (parentChildCount != 1) { - // Don't remove if the node has siblings - return; - } - - // - A parent can be removed if it doesn't have a background - // - A parent can be removed if has a background *and* the child does not have a - // background (in which case, just move the background over to the child, remove - // the parent) - // - If both child and parent have a background, the parent cannot be removed (a - // background can be translucent, have transparent padding, etc.) - boolean nodeHasBackground = element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND); - boolean parentHasBackground = parent.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND); - if (nodeHasBackground && parentHasBackground) { - // Can't remove because both define a background, and they might both be - // visible (e.g. through transparency or padding). - return; - } - - // Certain parents are special - such as the TabHost and the GestureOverlayView - - // where we want to leave things alone. - if (!CONTAINERS.contains(parentTag)) { - return; - } - - boolean hasId = element.hasAttributeNS(ANDROID_URI, ATTR_ID); - Location location = context.getLocation(element); - String tag = element.getTagName(); - String format; - if (hasId) { - format = "This %1$s layout or its %2$s parent is possibly useless"; - } else { - format = "This %1$s layout or its %2$s parent is useless"; - } - if (nodeHasBackground || parentHasBackground) { - format += "; transfer the background attribute to the other view"; - } - String message = String.format(format, tag, parentTag); - context.report(USELESS_PARENT, element, location, message, null); - } - - // This is the old UselessView check from layoutopt - private static void checkUselessLeaf(XmlContext context, Element element) { - assert LintUtils.getChildCount(element) == 0; - - // Conditions: - // - The node is a container view (LinearLayout, etc.) - // - The node has no id - // - The node has no background - // - The node has no children - // - The node has no style - // - The node is not a root - - if (element.hasAttributeNS(ANDROID_URI, ATTR_ID)) { - return; - } - - if (element.hasAttributeNS(ANDROID_URI, ATTR_BACKGROUND)) { - return; - } - - if (element.hasAttribute(ATTR_STYLE)) { - return; - } - - if (element == context.document.getDocumentElement()) { - return; - } - - Location location = context.getLocation(element); - String tag = element.getTagName(); - String message = String.format( - "This %1$s view is useless (no children, no background, no id, no style)", tag); - context.report(USELESS_LEAF, element, location, message, null); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Utf8Detector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Utf8Detector.java deleted file mode 100644 index 2e23483..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/Utf8Detector.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Document; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * Checks that the encoding used in resource files is always UTF-8. - */ -public class Utf8Detector extends LayoutDetector { - /** Detects non-utf8 encodings */ - public static final Issue ISSUE = Issue.create( - "EnforceUTF8", //$NON-NLS-1$ - "Checks that all XML resource files are using UTF-8 as the file encoding", - "XML supports encoding in a wide variety of character sets. However, not all " + - "tools handle the XML encoding attribute correctly, and nearly all Android " + - "apps use UTF-8, so by using UTF-8 you can protect yourself against subtle " + - "bugs when using non-ASCII characters.", - Category.I18N, - 2, - Severity.WARNING, - Utf8Detector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** See http://www.w3.org/TR/REC-xml/#NT-EncodingDecl */ - private static final Pattern ENCODING_PATTERN = - Pattern.compile("encoding=['\"](\\S*)['\"]");//$NON-NLS-1$ - - /** Constructs a new {@link Utf8Detector} */ - public Utf8Detector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - String xml = context.getContents(); - if (xml == null) { - return; - } - - // AAPT: The prologue must be in the first line - int lineEnd = 0; - int max = xml.length(); - for (; lineEnd < max; lineEnd++) { - char c = xml.charAt(lineEnd); - if (c == '\n' || c == '\r') { - break; - } - } - - for (int i = 16; i < lineEnd - 5; i++) { // +4: Skip at least <?xml encoding=" - if ((xml.charAt(i) == 'u' || xml.charAt(i) == 'U') - && (xml.charAt(i + 1) == 't' || xml.charAt(i + 1) == 'T') - && (xml.charAt(i + 2) == 'f' || xml.charAt(i + 2) == 'F') - && (xml.charAt(i + 3) == '-' || xml.charAt(i + 3) == '_') - && (xml.charAt(i + 4) == '8')) { - return; - } - } - - int encodingIndex = xml.lastIndexOf("encoding", lineEnd); //$NON-NLS-1$ - if (encodingIndex != -1) { - Matcher matcher = ENCODING_PATTERN.matcher(xml); - if (matcher.find(encodingIndex)) { - String encoding = matcher.group(1); - Location location = Location.create(context.file, xml, - matcher.start(1), matcher.end(1)); - context.report(ISSUE, null, location, String.format( - "%1$s: Not using UTF-8 as the file encoding. This can lead to subtle " + - "bugs with non-ascii characters", encoding), null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewConstructorDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewConstructorDetector.java deleted file mode 100644 index 4bffdd7..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewConstructorDetector.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright (C) 2012 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.CONSTRUCTOR_NAME; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -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 org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodNode; - -import java.io.File; -import java.util.List; - -/** - * Looks for custom views that do not define the view constructors needed by UI builders - */ -public class ViewConstructorDetector extends Detector implements Detector.ClassScanner { - private static final String SIG1 = - "(Landroid/content/Context;)V"; //$NON-NLS-1$ - private static final String SIG2 = - "(Landroid/content/Context;Landroid/util/AttributeSet;)V"; //$NON-NLS-1$ - private static final String SIG3 = - "(Landroid/content/Context;Landroid/util/AttributeSet;I)V"; //$NON-NLS-1$ - - /** The main issue discovered by this detector */ - public static final Issue ISSUE = Issue.create( - "ViewConstructor", //$NON-NLS-1$ - "Checks that custom views define the expected constructors", - - "Some layout tools (such as the Android layout editor for Eclipse) needs to " + - "find a constructor with one of the following signatures:\n" + - "* `View(Context context)`\n" + - "* `View(Context context, AttributeSet attrs)`\n" + - "* `View(Context context, AttributeSet attrs, int defStyle)`\n" + - "\n" + - "If your custom view needs to perform initialization which does not apply when " + - "used in a layout editor, you can surround the given code with a check to " + - "see if `View#isInEditMode()` is false, since that method will return `false` " + - "at runtime but true within a user interface editor.", - - Category.USABILITY, - 3, - Severity.WARNING, - ViewConstructorDetector.class, - Scope.CLASS_FILE_SCOPE); - - /** Constructs a new {@link ViewConstructorDetector} check */ - public ViewConstructorDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - public void checkClass(@NonNull ClassContext context, @NonNull ClassNode classNode) { - if (classNode.name.indexOf('$') != -1 - && (classNode.access & Opcodes.ACC_STATIC) == 0) { - // Ignore inner classes that aren't static: we can't create these - // anyway since we'd need the outer instance - return; - } - - // Ignore abstract classes - if ((classNode.access & Opcodes.ACC_ABSTRACT) != 0) { - return; - } - - if (isViewClass(context, classNode)) { - checkConstructors(context, classNode); - } - } - - private static boolean isViewClass(ClassContext context, ClassNode node) { - String superName = node.superName; - while (superName != null) { - if (superName.equals("android/view/View") //$NON-NLS-1$ - || superName.equals("android/view/ViewGroup") //$NON-NLS-1$ - || superName.startsWith("android/widget/") //$NON-NLS-1$ - && !((superName.endsWith("Adapter") //$NON-NLS-1$ - || superName.endsWith("Controller") //$NON-NLS-1$ - || superName.endsWith("Service") //$NON-NLS-1$ - || superName.endsWith("Provider") //$NON-NLS-1$ - || superName.endsWith("Filter")))) { //$NON-NLS-1$ - return true; - } - - superName = context.getDriver().getSuperClass(superName); - } - - return false; - } - - private static void checkConstructors(ClassContext context, ClassNode classNode) { - // Look through constructors - @SuppressWarnings("rawtypes") - List methods = classNode.methods; - for (Object methodObject : methods) { - MethodNode method = (MethodNode) methodObject; - if (method.name.equals(CONSTRUCTOR_NAME)) { - String desc = method.desc; - if (desc.equals(SIG1) || desc.equals(SIG2) || desc.equals(SIG3)) { - return; - } - } - } - - // If we get this far, none of the expected constructors were found. - - // Use location of one of the constructors? - String message = String.format( - "Custom view %1$s is missing constructor used by tools: " + - "(Context) or (Context,AttributeSet) or (Context,AttributeSet,int)", - classNode.name); - File sourceFile = context.getSourceFile(); - Location location = Location.create(sourceFile != null - ? sourceFile : context.file); - context.report(ISSUE, location, message, null /*data*/); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTagDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTagDetector.java deleted file mode 100644 index 46e24cc..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTagDetector.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (C) 2012 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 com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.client.api.LintDriver; -import com.android.tools.lint.client.api.SdkInfo; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -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 org.objectweb.asm.Type; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.BasicInterpreter; -import org.objectweb.asm.tree.analysis.BasicValue; -import org.objectweb.asm.tree.analysis.Frame; - -import java.util.Collections; -import java.util.EnumSet; -import java.util.List; - -/** - * Checks for missing view tag detectors - */ -public class ViewTagDetector extends Detector implements ClassScanner { - /** Using setTag and leaking memory */ - public static final Issue ISSUE = Issue.create( - "ViewTag", //$NON-NLS-1$ - "Finds potential leaks when using View.setTag", - - "Prior to Android 4.0, the implementation of View.setTag(int, Object) would " + - "store the objects in a static map, where the values were strongly referenced. " + - "This means that if the object contains any references pointing back to the " + - "context, the context (which points to pretty much everything else) will leak. " + - "If you pass a view, the view provides a reference to the context " + - "that created it. Similarly, view holders typically contain a view, and cursors " + - "are sometimes also associated with views.", - - Category.PERFORMANCE, - 6, - Severity.WARNING, - ViewTagDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.CLASS_FILE)); - - /** Constructs a new {@link ViewTagDetector} */ - public ViewTagDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Collections.singletonList("setTag"); //$NON-NLS-1$ - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - // The leak behavior is fixed in ICS: - // http://code.google.com/p/android/issues/detail?id=18273 - if (context.getMainProject().getMinSdk() >= 14) { - return; - } - - String owner = call.owner; - String desc = call.desc; - if (owner.equals("android/view/View") //$NON-NLS-1$ - && desc.equals("(ILjava/lang/Object;)V")) { //$NON-NLS-1$ - Analyzer analyzer = new Analyzer(new BasicInterpreter() { - @Override - public BasicValue newValue(Type type) { - if (type == null) { - return BasicValue.UNINITIALIZED_VALUE; - } else if (type.getSort() == Type.VOID) { - return null; - } else { - return new BasicValue(type); - } - } - }); - try { - Frame[] frames = analyzer.analyze(classNode.name, method); - InsnList instructions = method.instructions; - Frame frame = frames[instructions.indexOf(call)]; - if (frame.getStackSize() < 3) { - return; - } - BasicValue stackValue = (BasicValue) frame.getStack(2); - Type type = stackValue.getType(); - if (type == null) { - return; - } - - String internalName = type.getInternalName(); - String className = type.getClassName(); - LintDriver driver = context.getDriver(); - - SdkInfo sdkInfo = context.getClient().getSdkInfo(context.getMainProject()); - String objectType = null; - while (className != null) { - if (className.equals("android.view.View")) { //$NON-NLS-1$ - objectType = "views"; - break; - } else if (className.endsWith("ViewHolder")) { //$NON-NLS-1$ - objectType = "view holders"; - break; - } else if (className.endsWith("Cursor") //$NON-NLS-1$ - && className.startsWith("android.")) { //$NON-NLS-1$ - objectType = "cursors"; - break; - } - - // TBD: Bitmaps, drawables? That's tricky, because as explained in - // http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html - // apparently these are used along with nulling out the callbacks, - // and that's harder to detect here - - String parent = sdkInfo.getParentViewClass(className); - if (parent == null) { - if (internalName == null) { - internalName = className.replace('.', '/'); - } - assert internalName != null; - parent = driver.getSuperClass(internalName); - } - className = parent; - internalName = null; - } - - if (objectType != null) { - Location location = context.getLocation(call); - String message = String.format("Avoid setting %1$s as values for setTag: " + - "Can lead to memory leaks in versions older than Android 4.0", - objectType); - context.report(ISSUE, method, call, location, message, null); - } - } catch (AnalyzerException e) { - context.log(e, null); - } - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTypeDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTypeDetector.java deleted file mode 100644 index bad5d50..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/ViewTypeDetector.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ATTR_CLASS; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.DOT_JAVA; -import static com.android.SdkConstants.ID_PREFIX; -import static com.android.SdkConstants.NEW_ID_PREFIX; -import static com.android.SdkConstants.VIEW_TAG; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.resources.ResourceFolderType; -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.Issue; -import com.android.tools.lint.detector.api.JavaContext; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.ResourceXmlDetector; -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.XmlContext; -import com.google.common.base.Joiner; - -import org.w3c.dom.Attr; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import lombok.ast.AstVisitor; -import lombok.ast.Cast; -import lombok.ast.Expression; -import lombok.ast.MethodInvocation; -import lombok.ast.Select; -import lombok.ast.StrictListAccessor; - -/** Detector for finding inconsistent usage of views and casts */ -public class ViewTypeDetector extends ResourceXmlDetector implements Detector.JavaScanner { - /** Mismatched view types */ - public static final Issue ISSUE = Issue.create("WrongViewCast", //$NON-NLS-1$ - "Looks for incorrect casts to views that according to the XML are of a different type", - "Keeps track of the view types associated with ids and if it finds a usage of " + - "the id in the Java code it ensures that it is treated as the same type.", - Category.CORRECTNESS, - 9, - Severity.ERROR, - ViewTypeDetector.class, - EnumSet.of(Scope.ALL_RESOURCE_FILES, Scope.ALL_JAVA_FILES)); - - private final Map<String, Object> mIdToViewTag = new HashMap<String, Object>(50); - - @NonNull - @Override - public Speed getSpeed() { - return Speed.SLOW; - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT; - } - - @Override - public boolean appliesTo(@NonNull Context context, @NonNull File file) { - if (LintUtils.endsWith(file.getName(), DOT_JAVA)) { - return true; - } - - return super.appliesTo(context, file); - } - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_ID); - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - String view = attribute.getOwnerElement().getTagName(); - String value = attribute.getValue(); - String id = null; - if (value.startsWith(ID_PREFIX)) { - id = value.substring(ID_PREFIX.length()); - } else if (value.startsWith(NEW_ID_PREFIX)) { - id = value.substring(NEW_ID_PREFIX.length()); - } // else: could be @android id - - if (id != null) { - if (view.equals(VIEW_TAG)) { - view = attribute.getOwnerElement().getAttribute(ATTR_CLASS); - } - - Object existing = mIdToViewTag.get(id); - if (existing == null) { - mIdToViewTag.put(id, view); - } else if (existing instanceof String) { - String existingString = (String) existing; - if (!existingString.equals(view)) { - // Convert to list - List<String> list = new ArrayList<String>(2); - list.add((String) existing); - list.add(view); - mIdToViewTag.put(id, list); - } - } else if (existing instanceof List<?>) { - @SuppressWarnings("unchecked") - List<String> list = (List<String>) existing; - if (!list.contains(view)) { - list.add(view); - } - } - } - } - - // ---- Implements Detector.JavaScanner ---- - - @Override - public List<String> getApplicableMethodNames() { - return Collections.singletonList("findViewById"); //$NON-NLS-1$ - } - - @Override - public void visitMethod(@NonNull JavaContext context, @Nullable AstVisitor visitor, - @NonNull MethodInvocation node) { - assert node.astName().getDescription().equals("findViewById"); - if (node.getParent() instanceof Cast) { - Cast cast = (Cast) node.getParent(); - String castType = cast.astTypeReference().getTypeName(); - StrictListAccessor<Expression, MethodInvocation> args = node.astArguments(); - if (args.size() == 1) { - Expression first = args.first(); - // TODO: Do flow analysis as in the StringFormatDetector in order - // to handle variable references too - if (first instanceof Select) { - String resource = first.toString(); - if (resource.startsWith("R.id.")) { //$NON-NLS-1$ - String id = ((Select) first).astIdentifier().astValue(); - Object types = mIdToViewTag.get(id); - if (types instanceof String) { - String layoutType = (String) types; - checkCompatible(context, castType, layoutType, null, cast); - } else if (types instanceof List<?>) { - @SuppressWarnings("unchecked") - List<String> layoutTypes = (List<String>) types; - checkCompatible(context, castType, null, layoutTypes, cast); - } - } - } - } - } - } - - /** Check if the view and cast type are compatible */ - private static void checkCompatible(JavaContext context, String castType, String layoutType, - List<String> layoutTypes, Cast node) { - assert layoutType == null || layoutTypes == null; // Should only specify one or the other - boolean compatible = true; - if (layoutType != null) { - if (!layoutType.equals(castType) - && !context.getSdkInfo().isSubViewOf(castType, layoutType)) { - compatible = false; - } - } else { - compatible = false; - assert layoutTypes != null; - for (String type : layoutTypes) { - if (type.equals(castType) - || context.getSdkInfo().isSubViewOf(castType, type)) { - compatible = true; - break; - } - } - } - - if (!compatible) { - if (layoutType == null) { - layoutType = Joiner.on("|").join(layoutTypes); - } - String message = String.format( - "Unexpected cast to %1$s: layout tag was %2$s", - castType, layoutType); - context.report(ISSUE, node, context.parser.getLocation(context, node), message, - null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WakelockDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WakelockDetector.java deleted file mode 100644 index 47410eb..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WakelockDetector.java +++ /dev/null @@ -1,429 +0,0 @@ -/* - * Copyright (C) 2012 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.ANDROID_APP_ACTIVITY; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.checks.ControlFlowGraph.Node; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Scope; -import com.android.tools.lint.detector.api.Severity; - -import org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.InsnList; -import org.objectweb.asm.tree.JumpInsnNode; -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.analysis.AnalyzerException; - -import java.util.Arrays; -import java.util.List; - -/** - * Checks for problems with wakelocks (such as failing to release them) - * which can lead to unnecessary battery usage. - */ -public class WakelockDetector extends Detector implements ClassScanner { - - /** Problems using wakelocks */ - public static final Issue ISSUE = Issue.create( - "Wakelock", //$NON-NLS-1$ - "Looks for problems with wakelock usage", - - "Failing to release a wakelock properly can keep the Android device in " + - "a high power mode, which reduces battery life. There are several causes " + - "of this, such as releasing the wake lock in `onDestroy()` instead of in " + - "`onPause()`, failing to call `release()` in all possible code paths after " + - "an `acquire()`, and so on.\n" + - "\n" + - "NOTE: If you are using the lock just to keep the screen on, you should " + - "strongly consider using `FLAG_KEEP_SCREEN_ON` instead. This window flag " + - "will be correctly managed by the platform as the user moves between " + - "applications and doesn't require a special permission. See " + - "http://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_KEEP_SCREEN_ON.", - - Category.PERFORMANCE, - 9, - Severity.WARNING, - WakelockDetector.class, - Scope.CLASS_FILE_SCOPE); - - private static final String WAKELOCK_OWNER = "android/os/PowerManager$WakeLock"; //$NON-NLS-1$ - private static final String RELEASE_METHOD = "release"; //$NON-NLS-1$ - private static final String ACQUIRE_METHOD = "acquire"; //$NON-NLS-1$ - private static final String IS_HELD_METHOD = "isHeld"; //$NON-NLS-1$ - private static final String POWER_MANAGER = "android/os/PowerManager"; //$NON-NLS-1$ - private static final String NEW_WAKE_LOCK_METHOD = "newWakeLock"; //$NON-NLS-1$ - - /** Print diagnostics during analysis (display flow control graph etc). - * Make sure you add the asm-debug or asm-util jars to the runtime classpath - * as well since the opcode integer to string mapping display routine looks for - * it via reflection. */ - private static final boolean DEBUG = false; - - /** Constructs a new {@link WakelockDetector} */ - public WakelockDetector() { - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mHasAcquire && !mHasRelease && context.getDriver().getPhase() == 1) { - // Gather positions of the acquire calls - context.getDriver().requestRepeat(this, Scope.CLASS_FILE_SCOPE); - } - } - - // ---- Implements ClassScanner ---- - - /** Whether any {@code acquire()} calls have been encountered */ - private boolean mHasAcquire; - - /** Whether any {@code release()} calls have been encountered */ - private boolean mHasRelease; - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Arrays.asList(ACQUIRE_METHOD, RELEASE_METHOD, NEW_WAKE_LOCK_METHOD); - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - if (call.owner.equals(WAKELOCK_OWNER)) { - String name = call.name; - if (name.equals(ACQUIRE_METHOD)) { - mHasAcquire = true; - - if (context.getDriver().getPhase() == 2) { - assert !mHasRelease; - context.report(ISSUE, method, call, context.getLocation(call), - "Found a wakelock acquire() but no release() calls anywhere", - null); - } else { - assert context.getDriver().getPhase() == 1; - // Perform flow analysis in this method to see if we're - // performing an acquire/release block, where there are code paths - // between the acquire and release which can result in the - // release call not getting reached. - checkFlow(context, classNode, method, call); - } - } else if (name.equals(RELEASE_METHOD)) { - mHasRelease = true; - - // See if the release is happening in an onDestroy method, in an - // activity. - if ("onDestroy".equals(method.name) //$NON-NLS-1$ - && context.getDriver().isSubclassOf( - classNode, ANDROID_APP_ACTIVITY)) { - context.report(ISSUE, method, call, context.getLocation(call), - "Wakelocks should be released in onPause, not onDestroy", - null); - } - } - } else if (call.owner.equals(POWER_MANAGER)) { - if (call.name.equals(NEW_WAKE_LOCK_METHOD)) { - AbstractInsnNode prev = LintUtils.getPrevInstruction(call); - if (prev == null) { - return; - } - prev = LintUtils.getPrevInstruction(prev); - if (prev == null || prev.getOpcode() != Opcodes.LDC) { - return; - } - LdcInsnNode ldc = (LdcInsnNode) prev; - Object constant = ldc.cst; - if (constant instanceof Integer) { - int flag = ((Integer) constant).intValue(); - // Constant values are copied into the bytecode so we have to compare - // values; however, that means the values are part of the API - final int PARTIAL_WAKE_LOCK = 0x00000001; - final int ACQUIRE_CAUSES_WAKEUP = 0x10000000; - final int both = PARTIAL_WAKE_LOCK | ACQUIRE_CAUSES_WAKEUP; - if ((flag & both) == both) { - context.report(ISSUE, method, call, context.getLocation(call), - "Should not set both PARTIAL_WAKE_LOCK and ACQUIRE_CAUSES_WAKEUP. " - + "If you do not want the screen to turn on, get rid of " - + "ACQUIRE_CAUSES_WAKEUP", - null); - } - } - - } - } - } - - private void checkFlow(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode acquire) { - // Track allocations such that we know whether the type of the call - // is on a SecureRandom rather than a Random - final InsnList instructions = method.instructions; - MethodInsnNode release = null; - - // Find release call - for (int i = 0, n = instructions.size(); i < n; i++) { - AbstractInsnNode instruction = instructions.get(i); - int type = instruction.getType(); - if (type == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode call = (MethodInsnNode) instruction; - if (call.name.equals(RELEASE_METHOD) && - call.owner.equals(WAKELOCK_OWNER)) { - release = call; - break; - } - } - } - - if (release == null) { - // Didn't find both acquire and release in this method; no point in doing - // local flow analysis - return; - } - - try { - MyGraph graph = new MyGraph(); - ControlFlowGraph.create(graph, classNode, method); - - if (DEBUG) { - // Requires util package - //ClassNode clazz = classNode; - //clazz.accept(new TraceClassVisitor(new PrintWriter(System.out))); - System.out.println(graph.toString(graph.getNode(acquire))); - } - - int status = dfs(graph.getNode(acquire)); - if ((status & SEEN_RETURN) != 0) { - String message; - if ((status & SEEN_EXCEPTION) != 0) { - message = "The release() call is not always reached (via exceptional flow)"; - } else { - message = "The release() call is not always reached"; - } - - context.report(ISSUE, method, acquire, - context.getLocation(release), message, null); - } - } catch (AnalyzerException e) { - context.log(e, null); - } - } - - private static final int SEEN_TARGET = 1; - private static final int SEEN_BRANCH = 2; - private static final int SEEN_EXCEPTION = 4; - private static final int SEEN_RETURN = 8; - - /** TODO RENAME */ - private static class MyGraph extends ControlFlowGraph { - @Override - protected void add(@NonNull AbstractInsnNode from, @NonNull AbstractInsnNode to) { - if (from.getOpcode() == Opcodes.IFNULL) { - JumpInsnNode jump = (JumpInsnNode) from; - if (jump.label == to) { - // Skip jump targets on null if it's surrounding the release call - // - // if (lock != null) { - // lock.release(); - // } - // - // The above shouldn't be considered a scenario where release() may not - // be called. - AbstractInsnNode next = LintUtils.getNextInstruction(from); - if (next != null && next.getType() == AbstractInsnNode.VAR_INSN) { - next = LintUtils.getNextInstruction(next); - if (next != null && next.getType() == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode method = (MethodInsnNode) next; - if (method.name.equals(RELEASE_METHOD) && - method.owner.equals(WAKELOCK_OWNER)) { - // This isn't entirely correct; this will also trigger - // for "if (lock == null) { lock.release(); }" but that's - // not likely (and caught by other null checking in tools) - return; - } - } - } - } - } else if (from.getOpcode() == Opcodes.IFEQ) { - JumpInsnNode jump = (JumpInsnNode) from; - if (jump.label == to) { - AbstractInsnNode prev = LintUtils.getPrevInstruction(from); - if (prev != null && prev.getType() == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode method = (MethodInsnNode) prev; - if (method.name.equals(IS_HELD_METHOD) && - method.owner.equals(WAKELOCK_OWNER)) { - AbstractInsnNode next = LintUtils.getNextInstruction(from); - if (next != null) { - super.add(from, next); - return; - } - } - } - } - } - - super.add(from, to); - } - } - - /** Search from the given node towards the target; return false if we reach - * an exit point such as a return or a call on the way there that is not within - * a try/catch clause. - * - * @param node the current node - * @return true if the target was reached - * XXX RETURN VALUES ARE WRONG AS OF RIGHT NOW - */ - protected int dfs(ControlFlowGraph.Node node) { - AbstractInsnNode instruction = node.instruction; - if (instruction.getType() == AbstractInsnNode.JUMP_INSN) { - int opcode = instruction.getOpcode(); - if (opcode == Opcodes.RETURN || opcode == Opcodes.ARETURN - || opcode == Opcodes.LRETURN || opcode == Opcodes.IRETURN - || opcode == Opcodes.DRETURN || opcode == Opcodes.FRETURN - || opcode == Opcodes.ATHROW) { - if (DEBUG) { - System.out.println("Found exit via explicit return: " //$NON-NLS-1$ - + node.toString(false)); - } - return SEEN_RETURN; - } - } - - if (!DEBUG) { - // There are no cycles, so no *NEED* for this, though it does avoid - // researching shared labels. However, it makes debugging harder (no re-entry) - // so this is only done when debugging is off - if (node.visit != 0) { - return 0; - } - node.visit = 1; - } - - // Look for the target. This is any method call node which is a release on the - // lock (later also check it's the same instance, though that's harder). - // This is because finally blocks tend to be inlined so from a single try/catch/finally - // with a release() in the finally, the bytecode can contain multiple repeated - // (inlined) release() calls. - if (instruction.getType() == AbstractInsnNode.METHOD_INSN) { - MethodInsnNode method = (MethodInsnNode) instruction; - if (method.name.equals(RELEASE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) { - return SEEN_TARGET; - } else if (method.name.equals(ACQUIRE_METHOD) && method.owner.equals(WAKELOCK_OWNER)) { - // OK - } else if (method.name.equals(IS_HELD_METHOD) && method.owner.equals(WAKELOCK_OWNER)) { - // OK - } else { - // Some non acquire/release method call: if this is not associated with a - // try-catch block, it would mean the exception would exit the method, - // which would be an error - if (node.exceptions == null || node.exceptions.isEmpty()) { - // Look up the corresponding frame, if any - AbstractInsnNode curr = method.getPrevious(); - boolean foundFrame = false; - while (curr != null) { - if (curr.getType() == AbstractInsnNode.FRAME) { - foundFrame = true; - break; - } - curr = curr.getPrevious(); - } - - if (!foundFrame) { - if (DEBUG) { - System.out.println("Found exit via unguarded method call: " //$NON-NLS-1$ - + node.toString(false)); - } - return SEEN_RETURN; - } - } - } - } - - // if (node.instruction is a call, and the call is not caught by - // a try/catch block (provided the release is not inside the try/catch block) - // then return false - int status = 0; - - boolean implicitReturn = true; - List<Node> successors = node.successors; - List<Node> exceptions = node.exceptions; - if (exceptions != null) { - if (!exceptions.isEmpty()) { - implicitReturn = false; - } - for (Node successor : exceptions) { - status = dfs(successor) | status; - if ((status & SEEN_RETURN) != 0) { - if (DEBUG) { - System.out.println("Found exit via exception: " //$NON-NLS-1$ - + node.toString(false)); - } - return status; - } - } - - if (status != 0) { - status |= SEEN_EXCEPTION; - } - } - - if (successors != null) { - if (!successors.isEmpty()) { - implicitReturn = false; - if (successors.size() > 1) { - status |= SEEN_BRANCH; - } - } - for (Node successor : successors) { - status = dfs(successor) | status; - if ((status & SEEN_RETURN) != 0) { - if (DEBUG) { - System.out.println("Found exit via branches: " //$NON-NLS-1$ - + node.toString(false)); - } - return status; - } - } - } - - if (implicitReturn) { - status |= SEEN_RETURN; - if (DEBUG) { - System.out.println("Found exit: via implicit return: " //$NON-NLS-1$ - + node.toString(false)); - } - } - - return status; - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCallDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCallDetector.java deleted file mode 100644 index 174f915..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCallDetector.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2012 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.tools.lint.checks.JavaPerformanceDetector.ON_DRAW; -import static com.android.tools.lint.checks.JavaPerformanceDetector.ON_LAYOUT; -import static com.android.tools.lint.checks.JavaPerformanceDetector.ON_MEASURE; - -import com.android.annotations.NonNull; -import com.android.annotations.Nullable; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.ClassContext; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Detector.ClassScanner; -import com.android.tools.lint.detector.api.Issue; -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 org.objectweb.asm.Opcodes; -import org.objectweb.asm.tree.ClassNode; -import org.objectweb.asm.tree.MethodInsnNode; -import org.objectweb.asm.tree.MethodNode; - -import java.util.Arrays; -import java.util.List; - -/** - * Checks for cases where the wrong call is being made - */ -public class WrongCallDetector extends Detector implements ClassScanner { - /** Calling the wrong method */ - public static final Issue ISSUE = Issue.create( - "WrongCall", //$NON-NLS-1$ - "Finds cases where the wrong call is made, such as calling onMeasure instead of measure", - - "Custom views typically need to call `measure()` on their children, not `onMeasure`. " + - "Ditto for onDraw, onLayout, etc.", - - Category.CORRECTNESS, - 6, - Severity.ERROR, - WrongCallDetector.class, - Scope.CLASS_FILE_SCOPE); - - /** Constructs a new {@link WrongCallDetector} */ - public WrongCallDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements ClassScanner ---- - - @Override - @Nullable - public List<String> getApplicableCallNames() { - return Arrays.asList( - ON_DRAW, - ON_MEASURE, - ON_LAYOUT - ); - } - - @Override - public void checkCall(@NonNull ClassContext context, @NonNull ClassNode classNode, - @NonNull MethodNode method, @NonNull MethodInsnNode call) { - String name = call.name; - // Call is only allowed if it is both only called on the super class (invoke special) - // as well as within the same overriding method (e.g. you can't call super.onLayout - // from the onMeasure method) - if (call.getOpcode() != Opcodes.INVOKESPECIAL || !name.equals(method.name)) { - String suggestion = Character.toLowerCase(name.charAt(2)) + name.substring(3); - String message = String.format( - "Suspicious method call; should probably call \"%1$s\" rather than \"%2$s\"", - suggestion, name); - context.report(ISSUE, method, call, context.getLocation(call), message, null); - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCaseDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCaseDetector.java deleted file mode 100644 index d15af9a..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongCaseDetector.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright (C) 2013 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 com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Element; - -import java.util.Arrays; -import java.util.Collection; - -/** - * Check which looks for missing wrong case usage for certain layout tags. - * - * @todo Generalize this to handling spelling errors in general. - */ -public class WrongCaseDetector extends LayoutDetector { - /** Using the wrong case for layout tags */ - public static final Issue WRONGCASE = Issue.create( - "WrongCase", //$NON-NLS-1$ - "Ensures that the correct case is used for special layout tags such as <fragment>", - - "" - + "Most layout tags, such as <Button>, refer to actual view classes and are therefore " - + "capitalized. However, there are exceptions such as <fragment> and <include>. This " - + "lint check looks for incorrect capitalizations.", - - Category.CORRECTNESS, - 8, - Severity.WARNING, - WrongCaseDetector.class, - Scope.RESOURCE_FILE_SCOPE) - .setMoreInfo("http://developer.android.com/guide/components/fragments.html"); //$NON-NLS-1$ - - /** Constructs a new {@link WrongCaseDetector} */ - public WrongCaseDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList( - "Fragment", //$NON-NLS-1$ - "RequestFocus", //$NON-NLS-1$ - "Include", //$NON-NLS-1$ - "Merge" - ); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - String tag = element.getTagName(); - String correct = Character.toLowerCase(tag.charAt(0)) + tag.substring(1); - context.report(WRONGCASE, element, context.getLocation(element), - String.format("Invalid tag <%1$s>; should be <%2$s>", tag, correct), null); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongIdDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongIdDetector.java deleted file mode 100644 index e0e2d0a..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongIdDetector.java +++ /dev/null @@ -1,353 +0,0 @@ -/* - * 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.checks; - -import static com.android.SdkConstants.ANDROID_URI; -import static com.android.SdkConstants.ATTR_ID; -import static com.android.SdkConstants.ATTR_LAYOUT_RESOURCE_PREFIX; -import static com.android.SdkConstants.ATTR_NAME; -import static com.android.SdkConstants.ATTR_TYPE; -import static com.android.SdkConstants.ID_PREFIX; -import static com.android.SdkConstants.NEW_ID_PREFIX; -import static com.android.SdkConstants.RELATIVE_LAYOUT; -import static com.android.SdkConstants.TAG_ITEM; -import static com.android.SdkConstants.VALUE_ID; -import static com.android.tools.lint.detector.api.LintUtils.stripIdPrefix; - -import com.android.annotations.NonNull; -import com.android.resources.ResourceFolderType; -import com.android.tools.lint.client.api.IDomParser; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Context; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -import com.android.tools.lint.detector.api.LintUtils; -import com.android.tools.lint.detector.api.Location; -import com.android.tools.lint.detector.api.Location.Handle; -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.XmlContext; -import com.android.utils.Pair; -import com.google.common.base.Joiner; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; -import com.google.common.collect.Sets; - -import org.w3c.dom.Attr; -import org.w3c.dom.Element; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Checks for duplicate ids within a layout and within an included layout - */ -public class WrongIdDetector extends LayoutDetector { - - /** Ids bound to widgets in any of the layout files */ - private final Set<String> mGlobalIds = new HashSet<String>(100); - - /** Ids bound to widgets in the current layout file */ - private Set<String> mFileIds; - - /** Ids declared in a value's file, e.g. {@code <item type="id" name="foo"/>} */ - private Set<String> mDeclaredIds; - - /** - * Location handles for the various id references that were not found as - * defined in the same layout, to be checked after the whole project has - * been scanned - */ - private List<Pair<String, Location.Handle>> mHandles; - - /** List of RelativeLayout elements in the current layout */ - private List<Element> mRelativeLayouts; - - /** Reference to an unknown id */ - public static final Issue UNKNOWN_ID = Issue.create( - "UnknownId", //$NON-NLS-1$ - "Checks for id references in RelativeLayouts that are not defined elsewhere", - "The `@+id/` syntax refers to an existing id, or creates a new one if it has " + - "not already been defined elsewhere. However, this means that if you have a " + - "typo in your reference, or if the referred view no longer exists, you do not " + - "get a warning since the id will be created on demand. This check catches " + - "errors where you have renamed an id without updating all of the references to " + - "it.", - Category.CORRECTNESS, - 8, - Severity.FATAL, - WrongIdDetector.class, - Scope.ALL_RESOURCES_SCOPE); - - /** Reference to an id that is not in the current layout */ - public static final Issue UNKNOWN_ID_LAYOUT = Issue.create( - "UnknownIdInLayout", //$NON-NLS-1$ - "Makes sure that @+id references refer to views in the same layout", - - "The `@+id/` syntax refers to an existing id, or creates a new one if it has " + - "not already been defined elsewhere. However, this means that if you have a " + - "typo in your reference, or if the referred view no longer exists, you do not " + - "get a warning since the id will be created on demand.\n" + - "\n" + - "This is sometimes intentional, for example where you are referring to a view " + - "which is provided in a different layout via an include. However, it is usually " + - "an accident where you have a typo or you have renamed a view without updating " + - "all the references to it.", - - Category.CORRECTNESS, - 5, - Severity.WARNING, - WrongIdDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a duplicate id check */ - public WrongIdDetector() { - } - - @Override - public boolean appliesTo(@NonNull ResourceFolderType folderType) { - return folderType == ResourceFolderType.LAYOUT || folderType == ResourceFolderType.VALUES; - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public Collection<String> getApplicableAttributes() { - return Collections.singletonList(ATTR_ID); - } - - @Override - public Collection<String> getApplicableElements() { - return Arrays.asList(RELATIVE_LAYOUT, TAG_ITEM); - } - - @Override - public void beforeCheckFile(@NonNull Context context) { - mFileIds = new HashSet<String>(); - mRelativeLayouts = null; - } - - @Override - public void afterCheckFile(@NonNull Context context) { - if (mRelativeLayouts != null) { - if (!context.getProject().getReportIssues()) { - // If this is a library project not being analyzed, ignore it - return; - } - - for (Element layout : mRelativeLayouts) { - NodeList children = layout.getChildNodes(); - for (int j = 0, childCount = children.getLength(); j < childCount; j++) { - Node child = children.item(j); - if (child.getNodeType() != Node.ELEMENT_NODE) { - continue; - } - Element element = (Element) child; - NamedNodeMap attributes = element.getAttributes(); - for (int i = 0, n = attributes.getLength(); i < n; i++) { - Attr attr = (Attr) attributes.item(i); - String value = attr.getValue(); - if ((value.startsWith(NEW_ID_PREFIX) || - value.startsWith(ID_PREFIX)) - && ANDROID_URI.equals(attr.getNamespaceURI()) - && attr.getLocalName().startsWith(ATTR_LAYOUT_RESOURCE_PREFIX)) { - if (!idDefined(mFileIds, value)) { - // Stash a reference to this id and location such that - // we can check after the *whole* layout has been processed, - // since it's too early to conclude here that the id does - // not exist (you are allowed to have forward references) - XmlContext xmlContext = (XmlContext) context; - IDomParser parser = xmlContext.parser; - Handle handle = parser.createLocationHandle(xmlContext, attr); - handle.setClientData(attr); - - if (mHandles == null) { - mHandles = new ArrayList<Pair<String,Handle>>(); - } - mHandles.add(Pair.of(value, handle)); - } - } - } - } - } - } - - mFileIds = null; - } - - @Override - public void afterCheckProject(@NonNull Context context) { - if (mHandles != null) { - boolean checkSameLayout = context.isEnabled(UNKNOWN_ID_LAYOUT); - boolean checkExists = context.isEnabled(UNKNOWN_ID); - boolean projectScope = context.getScope().contains(Scope.ALL_RESOURCE_FILES); - for (Pair<String, Handle> pair : mHandles) { - String id = pair.getFirst(); - boolean isBound = idDefined(mGlobalIds, id); - if (!isBound && checkExists && projectScope) { - Handle handle = pair.getSecond(); - boolean isDeclared = idDefined(mDeclaredIds, id); - id = stripIdPrefix(id); - String suggestionMessage; - List<String> suggestions = getSpellingSuggestions(id, mGlobalIds); - if (suggestions.size() > 1) { - suggestionMessage = String.format(" Did you mean one of {%2$s} ?", - id, Joiner.on(", ").join(suggestions)); - } else if (!suggestions.isEmpty()) { - suggestionMessage = String.format(" Did you mean %2$s ?", - id, suggestions.get(0)); - } else { - suggestionMessage = ""; - } - String message; - if (isDeclared) { - message = String.format( - "The id \"%1$s\" is defined but not assigned to any views.%2$s", - id, suggestionMessage); - } else { - message = String.format( - "The id \"%1$s\" is not defined anywhere.%2$s", - id, suggestionMessage); - } - report(context, UNKNOWN_ID, handle, message); - } else if (checkSameLayout && (!projectScope || isBound) - && id.startsWith(NEW_ID_PREFIX)) { - // The id was defined, but in a different layout. Usually not intentional - // (might be referring to a random other view that happens to have the same - // name.) - Handle handle = pair.getSecond(); - report(context, UNKNOWN_ID_LAYOUT, handle, - String.format( - "The id \"%1$s\" is not referring to any views in this layout", - stripIdPrefix(id))); - } - } - } - } - - private static void report(Context context, Issue issue, Handle handle, String message) { - Location location = handle.resolve(); - Object clientData = handle.getClientData(); - if (clientData instanceof Node) { - if (context.getDriver().isSuppressed(issue, (Node) clientData)) { - return; - } - } - - context.report(issue, location, message, null); - } - - @Override - public void visitElement(@NonNull XmlContext context, @NonNull Element element) { - if (element.getTagName().equals(RELATIVE_LAYOUT)) { - if (mRelativeLayouts == null) { - mRelativeLayouts = new ArrayList<Element>(); - } - mRelativeLayouts.add(element); - } else { - assert element.getTagName().equals(TAG_ITEM); - String type = element.getAttribute(ATTR_TYPE); - if (VALUE_ID.equals(type)) { - String name = element.getAttribute(ATTR_NAME); - if (!name.isEmpty()) { - if (mDeclaredIds == null) { - mDeclaredIds = Sets.newHashSet(); - } - mDeclaredIds.add(ID_PREFIX + name); - } - } - } - } - - @Override - public void visitAttribute(@NonNull XmlContext context, @NonNull Attr attribute) { - assert attribute.getName().equals(ATTR_ID) || attribute.getLocalName().equals(ATTR_ID); - String id = attribute.getValue(); - mFileIds.add(id); - mGlobalIds.add(id); - } - - private static boolean idDefined(Set<String> ids, String id) { - if (ids == null) { - return false; - } - boolean definedLocally = ids.contains(id); - if (!definedLocally) { - if (id.startsWith(NEW_ID_PREFIX)) { - definedLocally = ids.contains(ID_PREFIX + - id.substring(NEW_ID_PREFIX.length())); - } else if (id.startsWith(ID_PREFIX)) { - definedLocally = ids.contains(NEW_ID_PREFIX + - id.substring(ID_PREFIX.length())); - } - } - - return definedLocally; - } - - private static List<String> getSpellingSuggestions(String id, Collection<String> ids) { - int maxDistance = id.length() >= 4 ? 2 : 1; - - // Look for typos and try to match with custom views and android views - Multimap<Integer, String> matches = ArrayListMultimap.create(2, 10); - int count = 0; - if (!ids.isEmpty()) { - for (String matchWith : ids) { - matchWith = stripIdPrefix(matchWith); - if (Math.abs(id.length() - matchWith.length()) > maxDistance) { - // The string lengths differ more than the allowed edit distance; - // no point in even attempting to compute the edit distance (requires - // O(n*m) storage and O(n*m) speed, where n and m are the string lengths) - continue; - } - int distance = LintUtils.editDistance(id, matchWith); - if (distance <= maxDistance) { - matches.put(distance, matchWith); - } - - if (count++ > 100) { - // Make sure that for huge projects we don't completely grind to a halt - break; - } - } - } - - for (int i = 0; i < maxDistance; i++) { - Collection<String> strings = matches.get(i); - if (strings != null && !strings.isEmpty()) { - List<String> suggestions = new ArrayList<String>(strings); - Collections.sort(suggestions); - return suggestions; - } - } - - return Collections.emptyList(); - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongImportDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongImportDetector.java deleted file mode 100644 index 64f3ce1..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongImportDetector.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.checks; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Detector; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.JavaContext; -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 java.util.Collections; -import java.util.List; - -import lombok.ast.AstVisitor; -import lombok.ast.ForwardingAstVisitor; -import lombok.ast.ImportDeclaration; -import lombok.ast.Node; - -/** - * Checks for "import android.R", which seems to be a common source of confusion - * (see for example http://stackoverflow.com/questions/885009/r-cannot-be-resolved-android-error - * and many other forums). - * <p> - * The root cause is probably this (from http://source.android.com/source/using-eclipse.html) : - * <blockquote> Note: Eclipse sometimes likes to add an import android.R - * statement at the top of your files that use resources, especially when you - * ask eclipse to sort or otherwise manage imports. This will cause your make to - * break. Look out for these erroneous import statements and delete them. - * </blockquote> - */ -public class WrongImportDetector extends Detector implements Detector.JavaScanner { - /** Is android.R being imported? */ - public static final Issue ISSUE = Issue.create("SuspiciousImport", //$NON-NLS-1$ - "Checks for 'import android.R' statements, which are usually accidental", - "Importing `android.R` is usually not intentional; it sometimes happens when " + - "you use an IDE and ask it to automatically add imports at a time when your " + - "project's R class it not present.\n" + - "\n" + - "Once the import is there you might get a lot of \"confusing\" error messages " + - "because of course the fields available on `android.R` are not the ones you'd " + - "expect from just looking at your own `R` class.", - Category.CORRECTNESS, - 9, - Severity.WARNING, - WrongImportDetector.class, - Scope.JAVA_FILE_SCOPE); - - /** Constructs a new {@link WrongImportDetector} check */ - public WrongImportDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - // ---- Implements Detector.JavaScanner ---- - - @Override - public List<Class<? extends Node>> getApplicableNodeTypes() { - return Collections.<Class<? extends Node>> singletonList( - ImportDeclaration.class); - } - - @Override - public AstVisitor createJavaVisitor(@NonNull JavaContext context) { - return new ImportVisitor(context); - } - - private static class ImportVisitor extends ForwardingAstVisitor { - private final JavaContext mContext; - - public ImportVisitor(JavaContext context) { - super(); - mContext = context; - } - - @Override - public boolean visitImportDeclaration(ImportDeclaration node) { - String fqn = node.asFullyQualifiedName(); - if (fqn.equals("android.R")) { //$NON-NLS-1$ - Location location = mContext.getLocation(node); - mContext.report(ISSUE, node, location, - "Don't include android.R here; use a fully qualified name for " - + "each usage instead", null); - } - return false; - } - } -} diff --git a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongLocationDetector.java b/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongLocationDetector.java deleted file mode 100644 index 37ffbeb..0000000 --- a/lint/libs/lint_checks/src/main/java/com/android/tools/lint/checks/WrongLocationDetector.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2012 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.TAG_RESOURCES; - -import com.android.annotations.NonNull; -import com.android.tools.lint.detector.api.Category; -import com.android.tools.lint.detector.api.Issue; -import com.android.tools.lint.detector.api.LayoutDetector; -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.XmlContext; - -import org.w3c.dom.Document; -import org.w3c.dom.Element; - -/** Looks for problems with XML files being placed in the wrong folder */ -public class WrongLocationDetector extends LayoutDetector { - /** Main issue investigated by this detector */ - public static final Issue ISSUE = Issue.create( - "WrongFolder", //$NON-NLS-1$ - - "Finds resource files that are placed in the wrong folders", - - "Resource files are sometimes placed in the wrong folder, and it can lead to " + - "subtle bugs that are hard to understand. This check looks for problems in this " + - "area, such as attempting to place a layout \"alias\" file in a `layout/` folder " + - "rather than the `values/` folder where it belongs.", - Category.CORRECTNESS, - 8, - Severity.ERROR, - WrongLocationDetector.class, - Scope.RESOURCE_FILE_SCOPE); - - /** Constructs a new {@link WrongLocationDetector} check */ - public WrongLocationDetector() { - } - - @NonNull - @Override - public Speed getSpeed() { - return Speed.FAST; - } - - @Override - public void visitDocument(@NonNull XmlContext context, @NonNull Document document) { - Element root = document.getDocumentElement(); - if (root != null && root.getTagName().equals(TAG_RESOURCES)) { - context.report(ISSUE, root, context.getLocation(root), - "This file should be placed in a values/ folder, not a layout/ folder", null); - } - } -} |