| /* |
| * Copyright (C) 2016 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_STYLE_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.ANDROID_URI; |
| import static com.android.SdkConstants.ATTR_DISCARD; |
| import static com.android.SdkConstants.ATTR_ID; |
| import static com.android.SdkConstants.ATTR_KEEP; |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.ATTR_PARENT; |
| import static com.android.SdkConstants.ATTR_SHRINK_MODE; |
| import static com.android.SdkConstants.ATTR_TYPE; |
| import static com.android.SdkConstants.PREFIX_ANDROID; |
| import static com.android.SdkConstants.PREFIX_BINDING_EXPR; |
| import static com.android.SdkConstants.PREFIX_RESOURCE_REF; |
| import static com.android.SdkConstants.PREFIX_THEME_REF; |
| import static com.android.SdkConstants.PREFIX_TWOWAY_BINDING_EXPR; |
| import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; |
| import static com.android.SdkConstants.TAG_ITEM; |
| import static com.android.SdkConstants.TAG_LAYOUT; |
| import static com.android.SdkConstants.TAG_STYLE; |
| import static com.android.SdkConstants.TOOLS_URI; |
| import static com.android.SdkConstants.VALUE_SAFE; |
| import static com.android.SdkConstants.VALUE_STRICT; |
| import static com.android.utils.SdkUtils.endsWithIgnoreCase; |
| import static com.google.common.base.Charsets.UTF_8; |
| |
| import com.android.SdkConstants; |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.ide.common.resources.ResourceUrl; |
| import com.android.resources.FolderTypeRelationship; |
| import com.android.resources.ResourceFolderType; |
| import com.android.resources.ResourceType; |
| import com.android.tools.lint.client.api.DefaultConfiguration; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Location; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.io.Files; |
| |
| 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.io.File; |
| import java.io.IOException; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.IdentityHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.regex.Pattern; |
| import java.util.regex.PatternSyntaxException; |
| |
| /** |
| * A model for Android resource declarations and usages |
| */ |
| public class ResourceUsageModel { |
| private static final int TYPICAL_RESOURCE_COUNT = 200; |
| |
| /** List of all known resources (parsed from R.java) */ |
| private final List<Resource> mResources = Lists.newArrayListWithExpectedSize(TYPICAL_RESOURCE_COUNT); |
| /** Map from resource type to map from resource name to resource object */ |
| private final Map<ResourceType, Map<String, Resource>> mTypeToName = |
| Maps.newEnumMap(ResourceType.class); |
| /** Map from R field value to corresponding resource */ |
| private final Map<Integer, Resource> mValueToResource = |
| Maps.newHashMapWithExpectedSize(TYPICAL_RESOURCE_COUNT); |
| |
| public static String getFieldName(Element element) { |
| return LintUtils.getFieldName(element.getAttribute(ATTR_NAME)); |
| } |
| |
| public static ResourceType getResourceType(Element element) { |
| String tagName = element.getTagName(); |
| if (tagName.equals(TAG_ITEM)) { |
| String typeName = element.getAttribute(ATTR_TYPE); |
| if (!typeName.isEmpty()) { |
| return ResourceType.getEnum(typeName); |
| } |
| } else if ("string-array".equals(tagName) || "integer-array".equals(tagName)) { |
| return ResourceType.ARRAY; |
| } else { |
| return ResourceType.getEnum(tagName); |
| } |
| return null; |
| } |
| |
| @Nullable |
| public Resource getResource(Element element) { |
| return getResource(element, false); |
| } |
| |
| public Resource getResource(Element element, boolean declare) { |
| ResourceType type = getResourceType(element); |
| if (type != null) { |
| String name = getFieldName(element); |
| Resource resource = getResource(type, name); |
| if (resource == null && declare) { |
| resource = addResource(type, name, null); |
| resource.setDeclared(true); |
| } |
| return resource; |
| } |
| |
| return null; |
| } |
| |
| @SuppressWarnings("unused") // Used by (temporary) copy in Gradle resource shrinker |
| @Nullable |
| public Resource getResource(@NonNull Integer value) { |
| return mValueToResource.get(value); |
| } |
| |
| @Nullable |
| public Resource getResource(@NonNull ResourceType type, @NonNull String name) { |
| Map<String, Resource> nameMap = mTypeToName.get(type); |
| if (nameMap != null) { |
| return nameMap.get(LintUtils.getFieldName(name)); |
| } |
| return null; |
| } |
| |
| @Nullable |
| Resource getResourceFromUrl(@NonNull String possibleUrlReference) { |
| ResourceUrl url = ResourceUrl.parse(possibleUrlReference); |
| if (url != null && !url.framework) { |
| return addResource(url.type, LintUtils.getFieldName(url.name), null); |
| } |
| |
| return null; |
| } |
| |
| private static final String ANDROID_RES = "android_res/"; |
| |
| @Nullable |
| public Resource getResourceFromFilePath(@NonNull String url) { |
| int nameSlash = url.lastIndexOf('/'); |
| if (nameSlash == -1) { |
| return null; |
| } |
| |
| // Look for |
| // (1) a full resource URL: /android_res/type/name.ext |
| // (2) a partial URL that uniquely identifies a given resource: drawable/name.ext |
| // e.g. file:///android_res/drawable/bar.png |
| int androidRes = url.indexOf(ANDROID_RES); |
| if (androidRes != -1) { |
| androidRes += ANDROID_RES.length(); |
| int slash = url.indexOf('/', androidRes); |
| if (slash != -1) { |
| String folderName = url.substring(androidRes, slash); |
| ResourceFolderType folderType = ResourceFolderType.getFolderType(folderName); |
| if (folderType != null) { |
| List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes( |
| folderType); |
| if (!types.isEmpty()) { |
| ResourceType type = types.get(0); |
| int nameBegin = slash + 1; |
| int dot = url.indexOf('.', nameBegin); |
| String name = url.substring(nameBegin, dot != -1 ? dot : url.length()); |
| return getResource(type, name); |
| } |
| } |
| } |
| } |
| |
| // Some other relative path. Just look from the end: |
| int typeSlash = url.lastIndexOf('/', nameSlash - 1); |
| ResourceType type = ResourceType.getEnum(url.substring(typeSlash + 1, nameSlash)); |
| if (type != null) { |
| int nameBegin = nameSlash + 1; |
| int dot = url.indexOf('.', nameBegin); |
| String name = url.substring(nameBegin, dot != -1 ? dot : url.length()); |
| return getResource(type, name); |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Marks the given resource (if non-null) as reachable, and returns true if |
| * this is the first time the resource is marked reachable |
| */ |
| public static boolean markReachable(@Nullable Resource resource) { |
| if (resource != null) { |
| boolean wasReachable = resource.isReachable(); |
| resource.setReachable(true); |
| return !wasReachable; |
| } |
| |
| return false; |
| } |
| |
| private static void markUnreachable(@Nullable Resource resource) { |
| if (resource != null) { |
| resource.setReachable(false); |
| } |
| } |
| |
| |
| public void recordManifestUsages(Node node) { |
| short nodeType = node.getNodeType(); |
| if (nodeType == Node.ELEMENT_NODE) { |
| Element element = (Element) node; |
| NamedNodeMap attributes = element.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Attr attr = (Attr) attributes.item(i); |
| markReachable(getResourceFromUrl(attr.getValue())); |
| } |
| } else if (nodeType == Node.TEXT_NODE) { |
| // Does this apply to any manifests?? |
| String text = node.getNodeValue().trim(); |
| markReachable(getResourceFromUrl(text)); |
| } |
| |
| NodeList children = node.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node child = children.item(i); |
| recordManifestUsages(child); |
| } |
| } |
| |
| private static final int RESOURCE_DECLARED = 1 << 1; |
| private static final int RESOURCE_PUBLIC = 1 << 2; |
| private static final int RESOURCE_KEEP = 1 << 3; |
| private static final int RESOURCE_DISCARD = 1 << 4; |
| private static final int RESOURCE_REACHABLE = 1 << 5; |
| |
| public static class Resource implements Comparable<Resource> { |
| private int mFlags; |
| |
| /** Type of resource */ |
| public final ResourceType type; |
| /** Name of resource */ |
| public final String name; |
| /** Integer id location */ |
| public int value; |
| |
| /** Resources this resource references. For example, a layout can reference another via |
| * an include; a style reference in a layout references that layout style, and so on. */ |
| public List<Resource> references; |
| |
| /** Chained list of declaration locations */ |
| public Location locations; |
| public List<File> declarations; |
| |
| /** Whether we found a declaration for this resource (otherwise we might have seen |
| * a reference to this before we came across its potential declaration, so we added it |
| * to the map, but we don't want to report unused resources for invalid resource |
| * references */ |
| public boolean isDeclared() { |
| return (mFlags & RESOURCE_DECLARED) != 0; |
| } |
| |
| /** Whether we found a declaration for this resource (otherwise we might have seen |
| * a reference to this before we came across its potential declaration, so we added it |
| * to the map, but we don't want to report unused resources for invalid resource |
| * references */ |
| public void setDeclared(boolean on) { |
| mFlags = on ? (mFlags | RESOURCE_DECLARED) : (mFlags & ~RESOURCE_DECLARED); |
| } |
| |
| /** This resource is marked as public */ |
| public boolean isPublic() { |
| return (mFlags & RESOURCE_PUBLIC) != 0; |
| } |
| |
| /** This resource is marked as public */ |
| public void setPublic(boolean on) { |
| mFlags = on ? (mFlags | RESOURCE_PUBLIC) : (mFlags & ~RESOURCE_PUBLIC); |
| } |
| |
| /** This resource is marked as to be ignored for usage analysis, regardless of |
| * references */ |
| public boolean isKeep() { |
| return (mFlags & RESOURCE_KEEP) != 0; |
| } |
| |
| /** This resource is marked as to be ignored for usage analysis, regardless of |
| * references */ |
| public void setKeep(boolean on) { |
| mFlags = on ? (mFlags | RESOURCE_KEEP) : (mFlags & ~RESOURCE_KEEP); |
| } |
| |
| /** This resource is marked as to be ignored for usage analysis, regardless of lack of |
| * references */ |
| public boolean isDiscard() { |
| return (mFlags & RESOURCE_DISCARD) != 0; |
| } |
| |
| /** This resource is marked as to be ignored for usage analysis, regardless of lack of |
| * references */ |
| public void setDiscard(boolean on) { |
| mFlags = on ? (mFlags | RESOURCE_DISCARD) : (mFlags & ~RESOURCE_DISCARD); |
| } |
| |
| public boolean isReachable() { |
| return (mFlags & RESOURCE_REACHABLE) != 0; |
| } |
| |
| public void setReachable(boolean on) { |
| mFlags = on ? (mFlags | RESOURCE_REACHABLE) : (mFlags & ~RESOURCE_REACHABLE); |
| } |
| |
| public Resource(ResourceType type, String name, int value) { |
| this.type = type; |
| this.name = name; |
| this.value = value; |
| } |
| |
| @Override |
| public String toString() { |
| return type + ":" + name + ":" + value; |
| } |
| |
| @SuppressWarnings("RedundantIfStatement") // Generated by IDE |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) { |
| return true; |
| } |
| if (o == null || getClass() != o.getClass()) { |
| return false; |
| } |
| |
| Resource resource = (Resource) o; |
| |
| if (name != null ? !name.equals(resource.name) : resource.name != null) { |
| return false; |
| } |
| if (type != resource.type) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| @Override |
| public int hashCode() { |
| int result = type != null ? type.hashCode() : 0; |
| result = 31 * result + (name != null ? name.hashCode() : 0); |
| return result; |
| } |
| |
| public void addLocation(@NonNull File file) { |
| if (declarations == null) { |
| declarations = Lists.newArrayList(); |
| } |
| declarations.add(file); |
| } |
| |
| public void recordLocation(@NonNull Location location) { |
| Location oldLocation = this.locations; |
| if (oldLocation != null) { |
| location.setSecondary(oldLocation); |
| } |
| this.locations = location; |
| } |
| |
| public void addReference(@Nullable Resource resource) { |
| if (resource != null) { |
| if (references == null) { |
| references = Lists.newArrayList(); |
| } else if (references.contains(resource)) { |
| return; |
| } |
| references.add(resource); |
| } |
| } |
| |
| public String getUrl() { |
| return '@' + type.getName() + '/' + name; |
| } |
| |
| public String getField() { |
| return "R." + type.getName() + '.' + name; |
| } |
| |
| @Override |
| public int compareTo(@NonNull Resource other) { |
| if (type != other.type) { |
| return type.compareTo(other.type); |
| } |
| |
| return name.compareTo(other.name); |
| } |
| } |
| |
| public List<Resource> findUnused() { |
| return findUnused(mResources); |
| } |
| |
| public String dumpReferences() { |
| StringBuilder sb = new StringBuilder(1000); |
| sb.append("Resource Reference Graph:\n"); |
| for (Resource resource : mResources) { |
| if (resource.references != null) { |
| sb.append(resource).append(" => ").append(resource.references).append('\n'); |
| } |
| } |
| return sb.toString(); |
| } |
| |
| |
| public String dumpResourceModel() { |
| StringBuilder sb = new StringBuilder(1000); |
| Collections.sort(mResources, new Comparator<Resource>() { |
| @Override |
| public int compare(Resource resource1, |
| Resource resource2) { |
| int delta = resource1.type.compareTo(resource2.type); |
| if (delta != 0) { |
| return delta; |
| } |
| return resource1.name.compareTo(resource2.name); |
| } |
| }); |
| |
| for (Resource resource : mResources) { |
| sb.append(resource.getUrl()).append(" : reachable=").append(resource.isReachable()); |
| sb.append("\n"); |
| if (resource.references != null) { |
| for (Resource referenced : resource.references) { |
| sb.append(" "); |
| sb.append(referenced.getUrl()); |
| sb.append("\n"); |
| } |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| public List<Resource> findUnused(List<Resource> resources) { |
| List<Resource> roots = findRoots(resources); |
| |
| Map<Resource,Boolean> seen = new IdentityHashMap<Resource,Boolean>(resources.size()); |
| for (Resource root : roots) { |
| visit(root, seen); |
| } |
| |
| List<Resource> unused = Lists.newArrayListWithExpectedSize(resources.size()); |
| for (Resource resource : resources) { |
| if (!resource.isReachable() |
| // Styles not yet handled correctly: don't mark as unused |
| && resource.type != ResourceType.ATTR |
| && resource.type != ResourceType.DECLARE_STYLEABLE |
| // Don't flag known service keys read by library |
| && !TranslationDetector.isServiceKey(resource.name)) { |
| unused.add(resource); |
| } |
| } |
| |
| return unused; |
| } |
| |
| @SuppressWarnings("MethodMayBeStatic") |
| @NonNull |
| protected List<Resource> findRoots(@NonNull List<Resource> resources) { |
| List<Resource> roots = Lists.newArrayList(); |
| |
| for (Resource resource : resources) { |
| if (resource.isReachable() || resource.isKeep()) { |
| roots.add(resource); |
| } |
| } |
| return roots; |
| } |
| |
| private static void visit(Resource root, Map<Resource, Boolean> seen) { |
| if (seen.containsKey(root)) { |
| return; |
| } |
| seen.put(root, Boolean.TRUE); |
| root.setReachable(true); |
| if (root.references != null) { |
| for (Resource referenced : root.references) { |
| visit(referenced, seen); |
| } |
| } |
| } |
| |
| @NonNull |
| public Resource addDeclaredResource(@NonNull ResourceType type, @NonNull String name, |
| @Nullable String value, boolean declared) { |
| Resource resource = addResource(type, name, value); |
| if (declared) { |
| resource.setDeclared(true); |
| } |
| return resource; |
| } |
| |
| @NonNull |
| public Resource addResource(@NonNull ResourceType type, @NonNull String name, |
| @Nullable String value) { |
| int realValue = value != null ? Integer.decode(value) : -1; |
| Resource resource = getResource(type, name); |
| if (resource != null) { |
| //noinspection VariableNotUsedInsideIf |
| if (value != null) { |
| if (resource.value == -1) { |
| resource.value = realValue; |
| } else { |
| assert realValue == resource.value; |
| } |
| } |
| return resource; |
| } |
| |
| resource = new Resource(type, name, realValue); |
| mResources.add(resource); |
| if (realValue != -1) { |
| mValueToResource.put(realValue, resource); |
| } |
| Map<String, Resource> nameMap = mTypeToName.get(type); |
| if (nameMap == null) { |
| nameMap = Maps.newHashMapWithExpectedSize(30); |
| mTypeToName.put(type, nameMap); |
| } |
| nameMap.put(name, resource); |
| |
| // TODO: Assert that we don't set the same resource multiple times to different values. |
| // Could happen if you pass in stale data! |
| |
| return resource; |
| } |
| |
| /** |
| * Called for a tools:keep attribute containing a resource URL where that resource name |
| * is not referencing a known resource |
| * |
| * @param value The keep value |
| */ |
| private void processKeepAttributes(@NonNull String value) { |
| // TODO: When nothing matches one of these attributes, mark it as unused too! |
| // Handle comma separated lists of URLs and globs |
| if (value.indexOf(',') != -1) { |
| for (String portion : Splitter.on(',').omitEmptyStrings().trimResults().split(value)) { |
| processKeepAttributes(portion); |
| } |
| return; |
| } |
| |
| ResourceUrl url = ResourceUrl.parse(value); |
| if (url == null || url.framework) { |
| return; |
| } |
| |
| Resource resource = getResource(url.type, url.name); |
| if (resource != null) { |
| markReachable(resource); |
| } else if (url.name.contains("*") || url.name.contains("?")) { |
| // Look for globbing patterns |
| String regexp = DefaultConfiguration.globToRegexp(LintUtils.getFieldName(url.name)); |
| try { |
| Pattern pattern = Pattern.compile(regexp); |
| Map<String, Resource> nameMap = mTypeToName.get(url.type); |
| if (nameMap != null) { |
| for (Resource r : nameMap.values()) { |
| if (pattern.matcher(r.name).matches()) { |
| markReachable(r); |
| } |
| } |
| } |
| } catch (PatternSyntaxException ignored) { |
| } |
| } |
| } |
| |
| private void processDiscardAttributes(@NonNull String value) { |
| // Handle comma separated lists of URLs and globs |
| if (value.indexOf(',') != -1) { |
| for (String portion : Splitter.on(',').omitEmptyStrings().trimResults().split(value)) { |
| processDiscardAttributes(portion); |
| } |
| return; |
| } |
| |
| ResourceUrl url = ResourceUrl.parse(value); |
| if (url == null || url.framework) { |
| return; |
| } |
| |
| Resource resource = getResource(url.type, url.name); |
| if (resource != null) { |
| markUnreachable(resource); |
| } else if (url.name.contains("*") || url.name.contains("?")) { |
| // Look for globbing patterns |
| String regexp = DefaultConfiguration.globToRegexp(LintUtils.getFieldName(url.name)); |
| try { |
| Pattern pattern = Pattern.compile(regexp); |
| Map<String, Resource> nameMap = mTypeToName.get(url.type); |
| if (nameMap != null) { |
| for (Resource r : nameMap.values()) { |
| if (pattern.matcher(r.name).matches()) { |
| markUnreachable(r); |
| } |
| } |
| } |
| } catch (PatternSyntaxException ignored) { |
| } |
| } |
| } |
| |
| /** |
| * Recorded list of keep attributes: these can contain wildcards, |
| * so they can't be applied immediately; we have to apply them after |
| * scanning through all resources (done by {@link #processToolsAttributes()} |
| */ |
| private List<String> mKeepAttributes; |
| |
| /** |
| * Recorded list of discard attributes: these can contain wildcards, |
| * so they can't be applied immediately; we have to apply them after |
| * scanning through all resources (done by {@link #processToolsAttributes()} |
| */ |
| private List<String> mDiscardAttributes; |
| |
| private boolean mSafeMode = true; |
| |
| /** |
| * Whether we should attempt to guess resources that should be kept based on looking |
| * at the string pool and assuming some of the strings can be used to dynamically construct |
| * the resource names. Can be turned off via {@code tools:shrinkMode="strict"}. |
| */ |
| public boolean isSafeMode() { |
| return mSafeMode; |
| } |
| |
| public void processToolsAttributes() { |
| if (mKeepAttributes != null) { |
| for (String keep : mKeepAttributes) { |
| processKeepAttributes(keep); |
| } |
| } |
| if (mDiscardAttributes != null) { |
| for (String discard : mDiscardAttributes) { |
| processDiscardAttributes(discard); |
| } |
| } |
| } |
| |
| public void recordToolsAttributes(@Nullable Attr attr) { |
| if (attr == null) { |
| return; |
| } |
| String localName = attr.getLocalName(); |
| String value = attr.getValue(); |
| if (ATTR_KEEP.equals(localName)) { |
| if (mKeepAttributes == null) { |
| mKeepAttributes = Lists.newArrayList(); |
| } |
| mKeepAttributes.add(value); |
| } else if (ATTR_DISCARD.equals(localName)) { |
| if (mDiscardAttributes == null) { |
| mDiscardAttributes = Lists.newArrayList(); |
| } |
| mDiscardAttributes.add(value); |
| } else if (ATTR_SHRINK_MODE.equals(localName)) { |
| if (VALUE_STRICT.equals(value)) { |
| mSafeMode = false; |
| } else if (VALUE_SAFE.equals(value)) { |
| mSafeMode = true; |
| } |
| } |
| } |
| |
| protected Resource declareResource(ResourceType type, String name, Node node) { |
| return addDeclaredResource(type, name, null, true); |
| } |
| |
| @NonNull |
| protected String readText(@NonNull File file) { |
| try { |
| return Files.toString(file, UTF_8); |
| } catch (IOException ignore) { |
| return ""; |
| } |
| } |
| |
| public void visitBinaryResource( |
| @Nullable ResourceFolderType folderType, |
| @NonNull File file) { |
| Resource from = null; |
| if (folderType != ResourceFolderType.VALUES) { |
| // Record resource for the whole file |
| List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes( |
| folderType); |
| ResourceType type = types.get(0); |
| assert type != ResourceType.ID : folderType; |
| String name = LintUtils.getBaseName(file.getName()); |
| from = declareResource(type, name, null); |
| } |
| |
| if (folderType == ResourceFolderType.RAW) { |
| // Is this an HTML, CSS or JavaScript document bundled with the app? |
| // If so tokenize and look for resource references. |
| String path = file.getPath(); |
| if (endsWithIgnoreCase(path, ".html") || endsWithIgnoreCase(path, ".htm")) { |
| tokenizeHtml(from, readText(file)); |
| } else if (endsWithIgnoreCase(path, ".css")) { |
| tokenizeCss(from, readText(file)); |
| } else if (endsWithIgnoreCase(path, ".js")) { |
| tokenizeJs(from, readText(file)); |
| } else if (file.isFile() && !LintUtils.isBitmapFile(file)) { |
| tokenizeUnknownBinary(from, file); |
| } |
| } |
| } |
| |
| public void visitXmlDocument( |
| @NonNull File file, |
| @Nullable ResourceFolderType folderType, |
| @NonNull Document document) { |
| if (folderType == null) { |
| // Manifest file |
| recordManifestUsages(document.getDocumentElement()); |
| return; |
| } |
| Resource from = null; |
| if (folderType != ResourceFolderType.VALUES) { |
| // Record resource for the whole file |
| List<ResourceType> types = FolderTypeRelationship.getRelatedResourceTypes( |
| folderType); |
| ResourceType type = types.get(0); |
| assert type != ResourceType.ID : folderType; |
| String name = LintUtils.getBaseName(file.getName()); |
| |
| from = declareResource(type, name, document.getDocumentElement()); |
| } else if (isAnalyticsFile(file)) { |
| return; |
| } |
| |
| // For value files, and drawables and colors etc also pull in resource |
| // references inside the context.file |
| recordResourceReferences(folderType, document.getDocumentElement(), from); |
| |
| if (folderType == ResourceFolderType.XML) { |
| tokenizeUnknownText(readText(file)); |
| } |
| } |
| |
| 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 file the file in question |
| * @return true if the file represents an analytics file |
| */ |
| public static boolean isAnalyticsFile(File file) { |
| return file.getPath().endsWith(ANALYTICS_FILE) && file.getName().equals(ANALYTICS_FILE); |
| } |
| |
| /** |
| * Records resource declarations and usages within an XML resource file |
| * @param folderType the type of resource file |
| * @param node the root node to start the recursive search from |
| * @param from a referencing context, if any. |
| */ |
| public void recordResourceReferences( |
| @NonNull ResourceFolderType folderType, |
| @NonNull Node node, |
| @Nullable Resource from) { |
| short nodeType = node.getNodeType(); |
| if (nodeType == Node.ELEMENT_NODE) { |
| Element element = (Element) node; |
| if (from != null) { |
| NamedNodeMap attributes = element.getAttributes(); |
| for (int i = 0, n = attributes.getLength(); i < n; i++) { |
| Attr attr = (Attr) attributes.item(i); |
| |
| // Ignore tools: namespace attributes, unless it's |
| // a keep attribute |
| if (TOOLS_URI.equals(attr.getNamespaceURI())) { |
| recordToolsAttributes(attr); |
| // Skip all other tools: attributes |
| continue; |
| } |
| |
| String value = attr.getValue(); |
| if (!(value.startsWith(PREFIX_RESOURCE_REF) |
| || value.startsWith(PREFIX_THEME_REF))) { |
| continue; |
| } |
| ResourceUrl url = ResourceUrl.parse(value); |
| if (url != null && !url.framework) { |
| Resource resource; |
| if (url.create) { |
| boolean isId = ATTR_ID.equals(attr.getLocalName()); |
| if (isId && TAG_LAYOUT.equals( |
| element.getOwnerDocument().getDocumentElement().getTagName())) { |
| // When using data binding (root <layout> tag) the id's will be |
| // automatically bound (the binder will look through the layout |
| // and find all the id's.) Therefore, treat these as read for |
| // now; longer term, it would be cool if we could track uses of |
| // the binding field instead. |
| markReachable(addResource(url.type, url.name, null)); |
| } else { |
| resource = declareResource(url.type, url.name, attr); |
| if (!isId || !ANDROID_URI.equals(attr.getNamespaceURI())) { |
| // Declaring an id is not a reference to that id |
| from.addReference(resource); |
| } |
| } |
| } else { |
| resource = addResource(url.type, url.name, null); |
| from.addReference(resource); |
| } |
| } else if (value.startsWith(PREFIX_BINDING_EXPR) || |
| value.startsWith(PREFIX_TWOWAY_BINDING_EXPR)) { |
| // Data binding expression: there could be multiple references here |
| int length = value.length(); |
| int index = value.startsWith(PREFIX_TWOWAY_BINDING_EXPR) |
| ? PREFIX_TWOWAY_BINDING_EXPR.length() |
| : PREFIX_BINDING_EXPR.length(); |
| while (true) { |
| index = value.indexOf('@', index); |
| if (index == -1) { |
| break; |
| } |
| // Find end of (potential) resource URL: first non resource URL character |
| int end = index + 1; |
| while (end < length) { |
| char c = value.charAt(end); |
| if (!(Character.isJavaIdentifierPart(c) || |
| c == '_' || |
| c == '.' || |
| c == '/' || |
| c == '+')) { |
| break; |
| } |
| end++; |
| } |
| url = ResourceUrl.parse(value.substring(index, end)); |
| if (url != null && !url.framework) { |
| Resource resource; |
| if (url.create) { |
| resource = declareResource(url.type, url.name, attr); |
| } else { |
| resource = addResource(url.type, url.name, null); |
| } |
| from.addReference(resource); |
| } |
| |
| index = end; |
| } |
| } |
| } |
| |
| // Android Wear. We *could* limit ourselves to only doing this in files |
| // referenced from a manifest meta-data element, e.g. |
| // <meta-data android:name="com.google.android.wearable.beta.app" |
| // android:resource="@xml/wearable_app_desc"/> |
| // but given that that property has "beta" in the name, it seems likely |
| // to change and therefore hardcoding it for that key risks breakage |
| // in the future. |
| if ("rawPathResId".equals(element.getTagName())) { |
| StringBuilder sb = new StringBuilder(); |
| NodeList children = node.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node child = children.item(i); |
| if (child.getNodeType() == Node.TEXT_NODE |
| || child.getNodeType() == Node.CDATA_SECTION_NODE) { |
| sb.append(child.getNodeValue()); |
| } |
| } |
| if (sb.length() > 0) { |
| Resource resource = getResource(ResourceType.RAW, sb.toString().trim()); |
| from.addReference(resource); |
| } |
| } |
| } else { |
| // Look for keep attributes everywhere else since they don't require a source |
| recordToolsAttributes(element.getAttributeNodeNS(TOOLS_URI, ATTR_KEEP)); |
| recordToolsAttributes(element.getAttributeNodeNS(TOOLS_URI, ATTR_DISCARD)); |
| recordToolsAttributes(element.getAttributeNodeNS(TOOLS_URI, ATTR_SHRINK_MODE)); |
| } |
| |
| if (folderType == ResourceFolderType.VALUES) { |
| |
| Resource definition = null; |
| ResourceType type = getResourceType(element); |
| if (type != null) { |
| String name = getFieldName(element); |
| if (type == ResourceType.PUBLIC) { |
| String typeName = element.getAttribute(ATTR_TYPE); |
| if (!typeName.isEmpty()) { |
| type = ResourceType.getEnum(typeName); |
| if (type != null) { |
| definition = declareResource(type, name, element); |
| definition.setPublic(true); |
| } |
| } |
| } else { |
| definition = declareResource(type, name, element); |
| } |
| } |
| if (definition != null) { |
| from = definition; |
| } |
| |
| String tagName = element.getTagName(); |
| if (TAG_STYLE.equals(tagName)) { |
| if (element.hasAttribute(ATTR_PARENT)) { |
| String parent = element.getAttribute(ATTR_PARENT); |
| if (!parent.isEmpty() && !parent.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) |
| && !parent.startsWith(PREFIX_ANDROID)) { |
| String parentStyle = parent; |
| if (!parentStyle.startsWith(STYLE_RESOURCE_PREFIX)) { |
| parentStyle = STYLE_RESOURCE_PREFIX + parentStyle; |
| } |
| Resource ps = getResourceFromUrl( |
| LintUtils.getFieldName(parentStyle)); |
| if (ps != null && definition != null) { |
| ps.addReference(definition); |
| definition.addReference(ps); |
| } |
| } else if (definition != null) { |
| // Extending a builtin theme: treat these as used |
| markReachable(definition); |
| } |
| } else { |
| // Implicit parent styles by name |
| String name = getFieldName(element); |
| while (true) { |
| int index = name.lastIndexOf('_'); |
| if (index != -1) { |
| name = name.substring(0, index); |
| Resource ps = getResourceFromUrl( |
| STYLE_RESOURCE_PREFIX + LintUtils.getFieldName(name)); |
| if (ps != null && definition != null) { |
| ps.addReference(definition); |
| definition.addReference(ps); |
| } |
| } else { |
| break; |
| } |
| } |
| } |
| } |
| |
| if (TAG_ITEM.equals(tagName)) { |
| // In style? If so the name: attribute can be a reference |
| if (element.getParentNode() != null |
| && element.getParentNode().getNodeName().equals(TAG_STYLE)) { |
| String name = element.getAttributeNS(ANDROID_URI, ATTR_NAME); |
| if (!name.isEmpty() && !name.startsWith("android:")) { |
| Resource resource = getResource(ResourceType.ATTR, name); |
| if (definition == null) { |
| Element style = (Element) element.getParentNode(); |
| definition = getResource(style); |
| if (definition != null) { |
| from = definition; |
| definition.addReference(resource); |
| } |
| } |
| } |
| } |
| } |
| } |
| } else if (nodeType == Node.TEXT_NODE || nodeType == Node.CDATA_SECTION_NODE) { |
| String text = node.getNodeValue().trim(); |
| // Why are we calling getFieldName here? That doesn't make sense! for styles I guess |
| Resource textResource = getResourceFromUrl( |
| LintUtils.getFieldName(text)); |
| if (textResource != null && from != null) { |
| from.addReference(textResource); |
| } |
| } |
| |
| NodeList children = node.getChildNodes(); |
| for (int i = 0, n = children.getLength(); i < n; i++) { |
| Node child = children.item(i); |
| recordResourceReferences(folderType, child, from); |
| } |
| } |
| |
| public void tokenizeHtml(@Nullable Resource from, @NonNull String html) { |
| // Look for |
| // (1) URLs of the form /android_res/drawable/foo.ext |
| // which we will use to keep R.drawable.foo |
| // and |
| // (2) Filenames. If the web content is loaded with something like |
| // WebView.loadDataWithBaseURL("file:///android_res/drawable/", ...) |
| // this is similar to Resources#getIdentifier handling where all |
| // *potentially* aliased filenames are kept to play it safe. |
| |
| // Simple HTML tokenizer |
| int length = html.length(); |
| final int STATE_TEXT = 1; |
| final int STATE_SLASH = 2; |
| final int STATE_ATTRIBUTE_NAME = 3; |
| final int STATE_BEFORE_TAG = 4; |
| final int STATE_IN_TAG = 5; |
| final int STATE_BEFORE_ATTRIBUTE = 6; |
| final int STATE_ATTRIBUTE_BEFORE_EQUALS = 7; |
| final int STATE_ATTRIBUTE_AFTER_EQUALS = 8; |
| final int STATE_ATTRIBUTE_VALUE_NONE = 9; |
| final int STATE_ATTRIBUTE_VALUE_SINGLE = 10; |
| final int STATE_ATTRIBUTE_VALUE_DOUBLE = 11; |
| final int STATE_CLOSE_TAG = 12; |
| |
| int state = STATE_TEXT; |
| int offset = 0; |
| int valueStart = 0; |
| int tagStart = 0; |
| String tag = null; |
| String attribute = null; |
| int attributeStart = 0; |
| int prev = -1; |
| while (offset < length) { |
| if (offset == prev) { |
| // Purely here to prevent potential bugs in the state machine from looping |
| // infinitely |
| offset++; |
| } |
| prev = offset; |
| |
| |
| char c = html.charAt(offset); |
| |
| // MAke sure I handle doctypes properly. |
| // Make sure I handle cdata properly. |
| // Oh and what about <style> tags? tokenize everything inside as CSS! |
| // ANd <script> tag content as js! |
| switch (state) { |
| case STATE_TEXT: { |
| if (c == '<') { |
| state = STATE_SLASH; |
| offset++; |
| continue; |
| } |
| |
| // Other text is just ignored |
| offset++; |
| break; |
| } |
| |
| case STATE_SLASH: { |
| if (c == '!') { |
| if (html.startsWith("!--", offset)) { |
| // Comment |
| int end = html.indexOf("-->", offset + 3); |
| if (end == -1) { |
| offset = length; |
| break; |
| } |
| offset = end + 3; |
| continue; |
| } else if (html.startsWith("![CDATA[", offset)) { |
| // Skip CDATA text content; HTML text is irrelevant to this tokenizer |
| // anyway |
| int end = html.indexOf("]]>", offset + 8); |
| if (end == -1) { |
| offset = length; |
| break; |
| } |
| offset = end + 3; |
| continue; |
| } |
| } else if (c == '/') { |
| state = STATE_CLOSE_TAG; |
| offset++; |
| continue; |
| } else if (c == '?') { |
| // XML Prologue |
| int end = html.indexOf('>', offset + 2); |
| if (end == -1) { |
| offset = length; |
| break; |
| } |
| offset = end + 1; |
| continue; |
| } |
| state = STATE_IN_TAG; |
| tagStart = offset; |
| break; |
| } |
| |
| case STATE_CLOSE_TAG: { |
| if (c == '>') { |
| state = STATE_TEXT; |
| } |
| offset++; |
| break; |
| } |
| |
| case STATE_BEFORE_TAG: { |
| if (!Character.isWhitespace(c)) { |
| state = STATE_IN_TAG; |
| tagStart = offset; |
| } |
| // (For an end tag we'll include / in the tag name here) |
| offset++; |
| break; |
| } |
| case STATE_IN_TAG: { |
| if (Character.isWhitespace(c)) { |
| state = STATE_BEFORE_ATTRIBUTE; |
| tag = html.substring(tagStart, offset).trim(); |
| } else if (c == '>') { |
| tag = html.substring(tagStart, offset).trim(); |
| endHtmlTag(from, html, offset, tag); |
| state = STATE_TEXT; |
| } |
| offset++; |
| break; |
| } |
| case STATE_BEFORE_ATTRIBUTE: { |
| if (c == '>') { |
| endHtmlTag(from, html, offset, tag); |
| state = STATE_TEXT; |
| } else //noinspection StatementWithEmptyBody |
| if (c == '/') { |
| // we expect an '>' next to close the tag |
| } else if (!Character.isWhitespace(c)) { |
| state = STATE_ATTRIBUTE_NAME; |
| attributeStart = offset; |
| } |
| offset++; |
| break; |
| } |
| case STATE_ATTRIBUTE_NAME: { |
| if (c == '>') { |
| endHtmlTag(from, html, offset, tag); |
| state = STATE_TEXT; |
| } else if (c == '=') { |
| attribute = html.substring(attributeStart, offset); |
| state = STATE_ATTRIBUTE_AFTER_EQUALS; |
| } else if (Character.isWhitespace(c)) { |
| attribute = html.substring(attributeStart, offset); |
| state = STATE_ATTRIBUTE_BEFORE_EQUALS; |
| } |
| offset++; |
| break; |
| } |
| case STATE_ATTRIBUTE_BEFORE_EQUALS: { |
| if (c == '=') { |
| state = STATE_ATTRIBUTE_AFTER_EQUALS; |
| } else if (c == '>') { |
| endHtmlTag(from, html, offset, tag); |
| state = STATE_TEXT; |
| } else if (!Character.isWhitespace(c)) { |
| // Attribute value not specified (used for some boolean attributes) |
| state = STATE_ATTRIBUTE_NAME; |
| attributeStart = offset; |
| } |
| offset++; |
| break; |
| } |
| |
| case STATE_ATTRIBUTE_AFTER_EQUALS: { |
| if (c == '\'') { |
| // a='b' |
| state = STATE_ATTRIBUTE_VALUE_SINGLE; |
| valueStart = offset + 1; |
| } else if (c == '"') { |
| // a="b" |
| state = STATE_ATTRIBUTE_VALUE_DOUBLE; |
| valueStart = offset + 1; |
| } else if (!Character.isWhitespace(c)) { |
| // a=b |
| state = STATE_ATTRIBUTE_VALUE_NONE; |
| valueStart = offset + 1; |
| } |
| offset++; |
| break; |
| } |
| |
| case STATE_ATTRIBUTE_VALUE_SINGLE: { |
| if (c == '\'') { |
| state = STATE_BEFORE_ATTRIBUTE; |
| recordHtmlAttributeValue(from, tag, attribute, |
| html.substring(valueStart, offset)); |
| } |
| offset++; |
| break; |
| } |
| case STATE_ATTRIBUTE_VALUE_DOUBLE: { |
| if (c == '"') { |
| state = STATE_BEFORE_ATTRIBUTE; |
| recordHtmlAttributeValue(from, tag, attribute, |
| html.substring(valueStart, offset)); |
| } |
| offset++; |
| break; |
| } |
| case STATE_ATTRIBUTE_VALUE_NONE: { |
| if (c == '>') { |
| recordHtmlAttributeValue(from, tag, attribute, |
| html.substring(valueStart, offset)); |
| endHtmlTag(from, html, offset, tag); |
| state = STATE_TEXT; |
| } else if (Character.isWhitespace(c)) { |
| state = STATE_BEFORE_ATTRIBUTE; |
| recordHtmlAttributeValue(from, tag, attribute, |
| html.substring(valueStart, offset)); |
| } |
| offset++; |
| break; |
| } |
| default: |
| assert false : state; |
| } |
| } |
| } |
| |
| private void endHtmlTag(@Nullable Resource from, @NonNull String html, int offset, |
| @Nullable String tag) { |
| if ("script".equals(tag)) { |
| int end = html.indexOf("</script>", offset + 1); |
| if (end != -1) { |
| // Attempt to tokenize the text as JavaScript |
| String js = html.substring(offset + 1, end); |
| tokenizeJs(from, js); |
| } |
| } else if ("style".equals(tag)) { |
| int end = html.indexOf("</style>", offset + 1); |
| if (end != -1) { |
| // Attempt to tokenize the text as CSS |
| String css = html.substring(offset + 1, end); |
| tokenizeCss(from, css); |
| } |
| } |
| } |
| |
| public void tokenizeJs(@Nullable Resource from, @NonNull String js) { |
| // Simple JavaScript tokenizer: only looks for literal strings, |
| // and records those as string references |
| int length = js.length(); |
| final int STATE_INIT = 1; |
| final int STATE_SLASH = 2; |
| final int STATE_STRING_DOUBLE = 3; |
| final int STATE_STRING_DOUBLE_QUOTED = 4; |
| final int STATE_STRING_SINGLE = 5; |
| final int STATE_STRING_SINGLE_QUOTED = 6; |
| |
| int state = STATE_INIT; |
| int offset = 0; |
| int stringStart = 0; |
| int prev = -1; |
| while (offset < length) { |
| if (offset == prev) { |
| // Purely here to prevent potential bugs in the state machine from looping |
| // infinitely |
| offset++; |
| } |
| prev = offset; |
| |
| char c = js.charAt(offset); |
| switch (state) { |
| case STATE_INIT: { |
| if (c == '/') { |
| state = STATE_SLASH; |
| } else if (c == '"') { |
| stringStart = offset + 1; |
| state = STATE_STRING_DOUBLE; |
| } else if (c == '\'') { |
| stringStart = offset + 1; |
| state = STATE_STRING_SINGLE; |
| } |
| offset++; |
| break; |
| } |
| case STATE_SLASH: { |
| if (c == '*') { |
| // Comment block |
| state = STATE_INIT; |
| int end = js.indexOf("*/", offset + 1); |
| if (end == -1) { |
| offset = length; // unterminated |
| break; |
| } |
| offset = end + 2; |
| continue; |
| } else if (c == '/') { |
| // Line comment |
| state = STATE_INIT; |
| int end = js.indexOf('\n', offset + 1); |
| if (end == -1) { |
| offset = length; |
| break; |
| } |
| offset = end + 1; |
| continue; |
| } else { |
| // division - just continue |
| state = STATE_INIT; |
| offset++; |
| break; |
| } |
| } |
| case STATE_STRING_DOUBLE: { |
| if (c == '"') { |
| recordJsString(js.substring(stringStart, offset)); |
| state = STATE_INIT; |
| } else if (c == '\\') { |
| state = STATE_STRING_DOUBLE_QUOTED; |
| } |
| offset++; |
| break; |
| } |
| case STATE_STRING_DOUBLE_QUOTED: { |
| state = STATE_STRING_DOUBLE; |
| offset++; |
| break; |
| } |
| case STATE_STRING_SINGLE: { |
| if (c == '\'') { |
| recordJsString(js.substring(stringStart, offset)); |
| state = STATE_INIT; |
| } else if (c == '\\') { |
| state = STATE_STRING_SINGLE_QUOTED; |
| } |
| offset++; |
| break; |
| } |
| case STATE_STRING_SINGLE_QUOTED: { |
| state = STATE_STRING_SINGLE; |
| offset++; |
| break; |
| } |
| default: |
| assert false : state; |
| } |
| } |
| } |
| |
| public void tokenizeCss(@Nullable Resource from, @NonNull String css) { |
| // Simple CSS tokenizer: Only looks for URL references, and records those |
| // filenames. Skips everything else (unrelated to images). |
| int length = css.length(); |
| final int STATE_INIT = 1; |
| final int STATE_SLASH = 2; |
| int state = STATE_INIT; |
| int offset = 0; |
| int prev = -1; |
| while (offset < length) { |
| if (offset == prev) { |
| // Purely here to prevent potential bugs in the state machine from looping |
| // infinitely |
| offset++; |
| } |
| prev = offset; |
| |
| char c = css.charAt(offset); |
| switch (state) { |
| case STATE_INIT: { |
| if (c == '/') { |
| state = STATE_SLASH; |
| } else if (c == 'u' && css.startsWith("url(", offset) && offset > 0) { |
| char prevChar = css.charAt(offset-1); |
| if (Character.isWhitespace(prevChar) || prevChar == ':') { |
| int end = css.indexOf(')', offset); |
| offset += 4; // skip url( |
| while (offset < length && Character.isWhitespace(css.charAt(offset))) { |
| offset++; |
| } |
| if (end != -1 && end > offset + 1) { |
| while (end > offset |
| && Character.isWhitespace(css.charAt(end - 1))) { |
| end--; |
| } |
| if ((css.charAt(offset) == '"' |
| && css.charAt(end - 1) == '"') |
| || (css.charAt(offset) == '\'' |
| && css.charAt(end - 1) == '\'')) { |
| // Strip " or ' |
| offset++; |
| end--; |
| } |
| recordCssUrl(from, css.substring(offset, end).trim()); |
| } |
| offset = end + 1; |
| continue; |
| } |
| |
| } |
| offset++; |
| break; |
| } |
| case STATE_SLASH: { |
| if (c == '*') { |
| // CSS comment? Skip the whole block rather than staying within the |
| // character tokenizer. |
| int end = css.indexOf("*/", offset + 1); |
| if (end == -1) { |
| offset = length; |
| break; |
| } |
| offset = end + 2; |
| continue; |
| } |
| state = STATE_INIT; |
| offset++; |
| break; |
| } |
| default: |
| assert false : state; |
| } |
| } |
| } |
| |
| private static byte[] sAndroidResBytes; |
| |
| /** Look through binary/unknown files looking for resource URLs */ |
| public void tokenizeUnknownBinary(@Nullable Resource from, @NonNull File file) { |
| try { |
| if (sAndroidResBytes == null) { |
| sAndroidResBytes = ANDROID_RES.getBytes(SdkConstants.UTF_8); |
| } |
| byte[] bytes = Files.toByteArray(file); |
| int index = 0; |
| while (index != -1) { |
| index = indexOf(bytes, sAndroidResBytes, index); |
| if (index != -1) { |
| index += sAndroidResBytes.length; |
| |
| // Find the end of the URL |
| int begin = index; |
| int end = begin; |
| for (; end < bytes.length; end++) { |
| byte c = bytes[end]; |
| if (c != '/' && !Character.isJavaIdentifierPart((char)c)) { |
| // android_res/raw/my_drawable.png => @raw/my_drawable |
| String url = "@" + new String(bytes, begin, end - begin, UTF_8); |
| Resource resource = getResourceFromUrl(url); |
| if (resource != null) { |
| if (from != null) { |
| from.addReference(resource); |
| } else { |
| markReachable(resource); |
| } |
| } |
| break; |
| } |
| } |
| } |
| } |
| } catch (IOException e) { |
| // Ignore |
| } |
| } |
| |
| /** |
| * Returns the index of the given target array in the first array, looking from the given |
| * index |
| */ |
| private static int indexOf(byte[] array, byte[] target, int fromIndex) { |
| outer: |
| for (int i = fromIndex; i < array.length - target.length + 1; i++) { |
| for (int j = 0; j < target.length; j++) { |
| if (array[i + j] != target[j]) { |
| continue outer; |
| } |
| } |
| return i; |
| } |
| return -1; |
| } |
| |
| /** Look through text files of unknown structure looking for resource URLs */ |
| private void tokenizeUnknownText(@NonNull String text) { |
| int index = 0; |
| while (index != -1) { |
| index = text.indexOf(ANDROID_RES, index); |
| if (index != -1) { |
| index += ANDROID_RES.length(); |
| |
| // Find the end of the URL |
| int begin = index; |
| int end = begin; |
| int length = text.length(); |
| for (; end < length; end++) { |
| char c = text.charAt(end); |
| if (c != '/' && !Character.isJavaIdentifierPart(c)) { |
| // android_res/raw/my_drawable.png => @raw/my_drawable |
| markReachable(getResourceFromUrl("@" + text.substring(begin, end))); |
| break; |
| } |
| } |
| } |
| } |
| } |
| |
| /** Adds the resource identifiers found in the given Java source code into the reference map */ |
| public void tokenizeJavaCode(@NonNull String s) { |
| if (s.length() <= 2) { |
| return; |
| } |
| |
| // Scan looking for R.{type}.name identifiers |
| // Extremely simple state machine which just avoids comments, line comments |
| // and strings, and outside of that records any R. identifiers it finds |
| int index = 0; |
| int length = s.length(); |
| |
| char c; |
| char next; |
| for (; index < length; index++) { |
| c = s.charAt(index); |
| if (index == length - 1) { |
| break; |
| } |
| next = s.charAt(index + 1); |
| if (Character.isWhitespace(c)) { |
| continue; |
| } |
| if (c == '/') { |
| if (next == '*') { |
| // Block comment |
| while (index < length - 2) { |
| if (s.charAt(index) == '*' && s.charAt(index + 1) == '/') { |
| break; |
| } |
| index++; |
| } |
| index++; |
| } else if (next == '/') { |
| // Line comment |
| while (index < length && s.charAt(index) != '\n') { |
| index++; |
| } |
| } |
| } else if (c == '\'') { |
| // Character |
| if (next == '\\') { |
| // Skip '\c' |
| index += 2; |
| } else { |
| // Skip 'c' |
| index++; |
| } |
| } else if (c == '\"') { |
| // String: Skip to end |
| index++; |
| while (index < length - 1) { |
| char t = s.charAt(index); |
| if (t == '\\') { |
| index++; |
| } else if (t == '"') { |
| break; |
| } |
| index++; |
| } |
| } else if (c == 'R' && next == '.') { |
| // This might be a pattern |
| int begin = index; |
| index += 2; |
| while (index < length) { |
| char t = s.charAt(index); |
| if (t == '.') { |
| String typeName = s.substring(begin + 2, index); |
| ResourceType type = ResourceType.getEnum(typeName); |
| if (type != null) { |
| index++; |
| begin = index; |
| while (index < length && |
| Character.isJavaIdentifierPart(s.charAt(index))) { |
| index++; |
| } |
| if (index > begin) { |
| String name = s.substring(begin, index); |
| Resource resource = addResource(type, name, null); |
| markReachable(resource); |
| } |
| } |
| index--; |
| break; |
| } else if (!Character.isJavaIdentifierStart(t)) { |
| break; |
| } |
| index++; |
| } |
| } else if (Character.isJavaIdentifierPart(c)) { |
| // Skip to the end of the identifier |
| while (index < length && Character.isJavaIdentifierPart(s.charAt(index))) { |
| index++; |
| } |
| // Back up so the next character can be checked to see if it's a " etc |
| index--; |
| } // else just punctuation/operators ( ) ; etc |
| } |
| } |
| |
| protected void referencedString(@NonNull String string) { |
| } |
| |
| private void recordCssUrl(@Nullable Resource from, @NonNull String value) { |
| if (!referencedUrl(from, value)) { |
| referencedString(value); |
| } |
| } |
| |
| /** |
| * See if the given URL is a URL that we can resolve to a specific resource; if so, |
| * record it and return true, otherwise returns false. |
| */ |
| private boolean referencedUrl(@Nullable Resource from, @NonNull String url) { |
| Resource resource = getResourceFromFilePath(url); |
| if (resource == null && url.indexOf('/') == -1) { |
| // URLs are often within the raw folder |
| resource = getResource(ResourceType.RAW, |
| LintUtils.getFieldName(LintUtils.getBaseName(url))); |
| } |
| if (resource != null) { |
| if (from != null) { |
| from.addReference(resource); |
| } else { |
| // We don't have an inclusion context, so just assume this resource is reachable |
| markReachable(resource); |
| } |
| return true; |
| } |
| |
| return false; |
| } |
| |
| private void recordHtmlAttributeValue(@Nullable Resource from, @Nullable String tagName, |
| @Nullable String attribute, @NonNull String value) { |
| if ("href".equals(attribute) || "src".equals(attribute)) { |
| // In general we'd need to unescape the HTML here (e.g. remove entities) but |
| // those wouldn't be valid characters in the resource name anyway |
| if (!referencedUrl(from, value)) { |
| referencedString(value); |
| } |
| |
| // If this document includes another, record the reachability of that script/resource |
| if (from != null) { |
| from.addReference(getResourceFromFilePath(attribute)); |
| } |
| } |
| } |
| |
| private void recordJsString(@NonNull String string) { |
| referencedString(string); |
| } |
| |
| public List<Resource> getResources() { |
| return mResources; |
| } |
| |
| @NonNull |
| public Collection<Map<String, Resource>> getResourceMaps() { |
| return mTypeToName.values(); |
| } |
| } |