com.android.tools.lint.detector.api.LintUtils Maven / Gradle / Ivy
/*
* 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.ANDROID_PREFIX;
import static com.android.SdkConstants.ANDROID_URI;
import static com.android.SdkConstants.ATTR_LOCALE;
import static com.android.SdkConstants.BIN_FOLDER;
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_WEBP;
import static com.android.SdkConstants.DOT_XML;
import static com.android.SdkConstants.ID_PREFIX;
import static com.android.SdkConstants.NEW_ID_PREFIX;
import static com.android.SdkConstants.TOOLS_URI;
import static com.android.SdkConstants.UTF_8;
import static com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER;
import static com.android.ide.common.resources.configuration.LocaleQualifier.BCP_47_PREFIX;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.builder.model.AndroidProject;
import com.android.builder.model.ApiVersion;
import com.android.ide.common.rendering.api.ItemResourceValue;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.ide.common.res2.AbstractResourceRepository;
import com.android.ide.common.res2.ResourceItem;
import com.android.ide.common.resources.ResourceUrl;
import com.android.ide.common.resources.configuration.FolderConfiguration;
import com.android.ide.common.resources.configuration.LocaleQualifier;
import com.android.resources.FolderTypeRelationship;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.SdkVersionInfo;
import com.android.sdklib.repository.FullRevision;
import com.android.tools.lint.client.api.LintClient;
import com.android.utils.PositionXmlParser;
import com.android.utils.SdkUtils;
import com.google.common.annotations.Beta;
import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
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.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Queue;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import lombok.ast.ImportDeclaration;
/**
* Useful utility methods related to lint.
*
* 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.
*/
@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 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 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) {
return SdkUtils.endsWithIgnoreCase(file.getPath(), DOT_XML);
}
/**
* Returns true if the given file represents a bitmap drawable file
*
* @param file the file to be checked
* @return true if the given file is an xml file
*/
public static boolean isBitmapFile(@NonNull File file) {
String path = file.getPath();
// endsWith(name, DOT_PNG) is also true for endsWith(name, DOT_9PNG)
return endsWith(path, DOT_PNG)
|| endsWith(path, DOT_JPG)
|| endsWith(path, DOT_GIF)
|| endsWith(path, DOT_JPEG)
|| endsWith(path, DOT_WEBP);
}
/**
* 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 getChildren(@NonNull Node node) {
NodeList childNodes = node.getChildNodes();
List children = new ArrayList(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 number 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(@Nullable String id1, @Nullable String id2) {
if (id1 == null || id2 == null || id1.isEmpty() || id2.isEmpty()) {
return false;
}
if (id1.startsWith(NEW_ID_PREFIX)) {
if (id2.startsWith(NEW_ID_PREFIX)) {
return id1.equals(id2);
} else {
assert id2.startsWith(ID_PREFIX) : id2;
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) : id1;
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.
*
* 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 splitPath(@NonNull String path) {
if (path.indexOf(';') != -1) {
return Splitter.on(';').omitEmptyStrings().trimResults().split(path);
}
List combined = new ArrayList();
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 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_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.
*
* 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 true if the given class node represents an anonymous inner class
*
* @param classNode the class to be checked
* @return true if the class appears to be an anonymous class
*/
public static boolean isAnonymousClass(@NonNull ClassNode classNode) {
if (classNode.outerClass == null) {
return false;
}
String name = classNode.name;
int index = name.lastIndexOf('$');
if (index == -1 || index == name.length() - 1) {
return false;
}
return Character.isDigit(name.charAt(index + 1));
}
/**
* 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 is a lint manifest file directory.
*
* @param dir the directory to check
* @return true if the directory contains a manifest file
*/
public static boolean isManifestFolder(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();
//noinspection ConstantConditions
if (dir != null && isManifestFolder(dir)) {
// Yes, it's a bin/ directory inside a real project: ignore this dir
return false;
}
}
}
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", b+eng-US, 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.indexOf('-') == -1) {
return null;
}
String locale = null;
for (String qualifier : QUALIFIER_SPLITTER.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;
} else if (qualifier.startsWith(BCP_47_PREFIX)) {
return qualifier;
}
}
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(
@Nullable lombok.ast.Node compilationUnit,
@NonNull String fullyQualifiedName) {
if (compilationUnit == null) {
return false;
}
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;
}
/**
* Looks up the resource values for the given attribute given a style. Note that
* this only looks project-level style values, it does not resume into the framework
* styles.
*/
@Nullable
public static List getStyleAttributes(
@NonNull Project project, @NonNull LintClient client,
@NonNull String styleUrl, @NonNull String namespace, @NonNull String attribute) {
if (!client.supportsProjectResources()) {
return null;
}
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return null;
}
ResourceUrl style = ResourceUrl.parse(styleUrl);
if (style == null || style.framework) {
return null;
}
List result = null;
Queue queue = new ArrayDeque();
queue.add(new ResourceValue(style.type, style.name, false));
Set seen = Sets.newHashSet();
int count = 0;
boolean isFrameworkAttribute = ANDROID_URI.equals(namespace);
while (count < 30 && !queue.isEmpty()) {
ResourceValue front = queue.remove();
String name = front.getName();
seen.add(name);
List items = resources.getResourceItem(front.getResourceType(), name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue rv = item.getResourceValue(false);
if (rv instanceof StyleResourceValue) {
StyleResourceValue srv = (StyleResourceValue) rv;
ItemResourceValue value = srv.getItem(attribute, isFrameworkAttribute);
if (value != null) {
if (result == null) {
result = Lists.newArrayList();
}
if (!result.contains(value)) {
result.add(value);
}
}
String parent = srv.getParentStyle();
if (parent != null && !parent.startsWith(ANDROID_PREFIX)) {
ResourceUrl p = ResourceUrl.parse(parent);
if (p != null && !p.framework && !seen.contains(p.name)) {
seen.add(p.name);
queue.add(new ResourceValue(ResourceType.STYLE, p.name,
false));
}
}
int index = name.lastIndexOf('.');
if (index > 0) {
String parentName = name.substring(0, index);
if (!seen.contains(parentName)) {
seen.add(parentName);
queue.add(new ResourceValue(ResourceType.STYLE, parentName,
false));
}
}
}
}
}
count++;
}
return result;
}
@Nullable
public static List getInheritedStyles(
@NonNull Project project, @NonNull LintClient client,
@NonNull String styleUrl) {
if (!client.supportsProjectResources()) {
return null;
}
AbstractResourceRepository resources = client.getProjectResources(project, true);
if (resources == null) {
return null;
}
ResourceUrl style = ResourceUrl.parse(styleUrl);
if (style == null || style.framework) {
return null;
}
List result = null;
Queue queue = new ArrayDeque();
queue.add(new ResourceValue(style.type, style.name, false));
Set seen = Sets.newHashSet();
int count = 0;
while (count < 30 && !queue.isEmpty()) {
ResourceValue front = queue.remove();
String name = front.getName();
seen.add(name);
List items = resources.getResourceItem(front.getResourceType(), name);
if (items != null) {
for (ResourceItem item : items) {
ResourceValue rv = item.getResourceValue(false);
if (rv instanceof StyleResourceValue) {
StyleResourceValue srv = (StyleResourceValue) rv;
if (result == null) {
result = Lists.newArrayList();
}
result.add(srv);
String parent = srv.getParentStyle();
if (parent != null && !parent.startsWith(ANDROID_PREFIX)) {
ResourceUrl p = ResourceUrl.parse(parent);
if (p != null && !p.framework && !seen.contains(p.name)) {
seen.add(p.name);
queue.add(new ResourceValue(ResourceType.STYLE, p.name,
false));
}
}
int index = name.lastIndexOf('.');
if (index > 0) {
String parentName = name.substring(0, index);
if (!seen.contains(parentName)) {
seen.add(parentName);
queue.add(new ResourceValue(ResourceType.STYLE, parentName,
false));
}
}
}
}
}
count++;
}
return result;
}
/** Returns true if the given two paths point to the same logical resource file within
* a source set. This means that it only checks the parent folder name and individual
* file name, not the path outside the parent folder.
*
* @param file1 the first file to compare
* @param file2 the second file to compare
* @return true if the two files have the same parent and file names
*/
public static boolean isSameResourceFile(@Nullable File file1, @Nullable File file2) {
if (file1 != null && file2 != null
&& file1.getName().equals(file2.getName())) {
File parent1 = file1.getParentFile();
File parent2 = file2.getParentFile();
if (parent1 != null && parent2 != null &&
parent1.getName().equals(parent2.getName())) {
return true;
}
}
return false;
}
/**
* Whether we should attempt to look up the prefix from the model. Set to false
* if we encounter a model which is too old.
*
* This is public such that code which for example syncs to a new gradle model
* can reset it.
*/
public static boolean sTryPrefixLookup = true;
/** Looks up the resource prefix for the given Gradle project, if possible */
@Nullable
public static String computeResourcePrefix(@Nullable AndroidProject project) {
try {
if (sTryPrefixLookup && project != null) {
return project.getResourcePrefix();
}
} catch (Exception e) {
// This happens if we're talking to an older model than 0.10
// Ignore; fall through to normal handling and never try again.
//noinspection AssignmentToStaticFieldFromInstanceMethod
sTryPrefixLookup = false;
}
return null;
}
/** Computes a suggested name given a resource prefix and resource name */
public static String computeResourceName(@NonNull String prefix, @NonNull String name) {
if (prefix.isEmpty()) {
return name;
} else if (name.isEmpty()) {
return prefix;
} else if (prefix.endsWith("_")) {
return prefix + name;
} else {
return prefix + Character.toUpperCase(name.charAt(0)) + name.substring(1);
}
}
/**
* Convert an {@link com.android.builder.model.ApiVersion} to a {@link
* com.android.sdklib.AndroidVersion}. The chief problem here is that the {@link
* com.android.builder.model.ApiVersion}, when using a codename, will not encode the
* corresponding API level (it just reflects the string entered by the user in the gradle file)
* so we perform a search here (since lint really wants to know the actual numeric API level)
*
* @param api the api version to convert
* @param targets if known, the installed targets (used to resolve platform codenames, only
* needed to resolve platforms newer than the tools since {@link
* com.android.sdklib.SdkVersionInfo} knows the rest)
* @return the corresponding version
*/
@NonNull
public static AndroidVersion convertVersion(
@NonNull ApiVersion api,
@Nullable IAndroidTarget[] targets) {
String codename = api.getCodename();
if (codename != null) {
AndroidVersion version = SdkVersionInfo.getVersion(codename, targets);
if (version != null) {
return version;
}
return new AndroidVersion(api.getApiLevel(), codename);
}
return new AndroidVersion(api.getApiLevel(), null);
}
/**
* Returns true if the given Gradle model is older than the given version number
*/
public static boolean isModelOlderThan(@Nullable AndroidProject project,
int major, int minor, int micro) {
if (project != null) {
String modelVersion = project.getModelVersion();
try {
FullRevision version = FullRevision.parseRevision(modelVersion);
if (version.getMajor() != major) {
return version.getMajor() < major;
}
if (version.getMinor() != minor) {
return version.getMinor() < minor;
}
return version.getMicro() < micro;
} catch (NumberFormatException e) {
// ignore
}
}
return false;
}
/**
* Looks for a certain string within a larger string, which should immediately follow
* the given prefix and immediately precede the given suffix.
*
* @param string the full string to search
* @param prefix the optional prefix to follow
* @param suffix the optional suffix to precede
* @return the corresponding substring, if present
*/
@Nullable
public static String findSubstring(@NonNull String string, @Nullable String prefix,
@Nullable String suffix) {
int start = 0;
if (prefix != null) {
start = string.indexOf(prefix);
if (start == -1) {
return null;
}
start += prefix.length();
}
if (suffix != null) {
int end = string.indexOf(suffix, start);
if (end == -1) {
return null;
}
return string.substring(start, end);
}
return string.substring(start);
}
/**
* Splits up the given message coming from a given string format (where the string
* format follows the very specific convention of having only strings formatted exactly
* with the format %n$s where n is between 1 and 9 inclusive, and each formatting parameter
* appears exactly once, and in increasing order.
*
* @param format the format string responsible for creating the error message
* @param errorMessage an error message formatted with the format string
* @return the specific values inserted into the format
*/
@NonNull
public static List getFormattedParameters(
@NonNull String format,
@NonNull String errorMessage) {
StringBuilder pattern = new StringBuilder(format.length());
int parameter = 1;
for (int i = 0, n = format.length(); i < n; i++) {
char c = format.charAt(i);
if (c == '%') {
// Only support formats of the form %n$s where n is 1 <= n <=9
assert i < format.length() - 4 : format;
assert format.charAt(i + 1) == ('0' + parameter) : format;
assert Character.isDigit(format.charAt(i + 1)) : format;
assert format.charAt(i + 2) == '$' : format;
assert format.charAt(i + 3) == 's' : format;
parameter++;
i += 3;
pattern.append("(.*)");
} else {
pattern.append(c);
}
}
try {
Pattern compile = Pattern.compile(pattern.toString());
Matcher matcher = compile.matcher(errorMessage);
if (matcher.find()) {
int groupCount = matcher.groupCount();
List parameters = Lists.newArrayListWithExpectedSize(groupCount);
for (int i = 1; i <= groupCount; i++) {
parameters.add(matcher.group(i));
}
return parameters;
}
} catch (PatternSyntaxException pse) {
// Internal error: string format is not valid. Should be caught by unit tests
// as a failure to return the formatted parameters.
}
return Collections.emptyList();
}
/**
* Escapes the given property file value (right hand side of property assignment)
* as required by the property file format (e.g. escapes colons and backslashes)
*
* @param value the value to be escaped
* @return the escaped value
*/
@NonNull
public static String escapePropertyValue(@NonNull String value) {
// Slow, stupid implementation, but is 100% compatible with Java's property file
// implementation
Properties properties = new Properties();
properties.setProperty("k", value); // key doesn't matter
StringWriter writer = new StringWriter();
try {
properties.store(writer, null);
String s = writer.toString();
int end = s.length();
// Writer inserts trailing newline
String lineSeparator = SdkUtils.getLineSeparator();
if (s.endsWith(lineSeparator)) {
end -= lineSeparator.length();
}
int start = s.indexOf('=');
assert start != -1 : s;
return s.substring(start + 1, end);
}
catch (IOException e) {
return value; // shouldn't happen; we're not going to disk
}
}
/**
* Returns the locale for the given parent folder.
*
* @param parent the name of the parent folder
* @return null if the locale is not known, or a locale qualifier providing the language
* and possibly region
*/
@Nullable
public static LocaleQualifier getLocale(@NonNull String parent) {
if (parent.indexOf('-') != -1) {
FolderConfiguration config = FolderConfiguration.getConfigForFolder(parent);
if (config != null) {
return config.getLocaleQualifier();
}
}
return null;
}
/**
* Returns the locale for the given context.
*
* @param context the context to look up the locale for
* @return null if the locale is not known, or a locale qualifier providing the language
* and possibly region
*/
@Nullable
public static LocaleQualifier getLocale(@NonNull XmlContext context) {
Element root = context.document.getDocumentElement();
if (root != null) {
String locale = root.getAttributeNS(TOOLS_URI, ATTR_LOCALE);
if (locale != null && !locale.isEmpty()) {
return getLocale(locale);
}
}
return getLocale(context.file.getParentFile().getName());
}
/**
* Check whether the given resource file is in an English locale
* @param context the XML context for the resource file
* @param assumeForBase whether the base folder (e.g. no locale specified) should be
* treated as English
*/
public static boolean isEnglishResource(@NonNull XmlContext context, boolean assumeForBase) {
LocaleQualifier locale = LintUtils.getLocale(context);
if (locale == null) {
return assumeForBase;
} else {
return "en".equals(locale.getLanguage()); //$NON-NLS-1$
}
}
}