blob: 2503b875cd705a6b642516d1f6e6483a81976773 [file] [log] [blame]
/*
* Copyright (C) 2015 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.DOT_CLASS;
import static com.android.SdkConstants.DOT_JAR;
import static org.objectweb.asm.Opcodes.ASM5;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.google.common.collect.Maps;
import com.google.common.io.ByteStreams;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
/** A class, present either as a .class file on disk, or inside a .jar file. */
@VisibleForTesting
class ClassEntry implements Comparable<ClassEntry> {
public final File file;
public final File jarFile;
public final File binDir;
public final byte[] bytes;
@VisibleForTesting
ClassEntry(
@NonNull File file,
@Nullable File jarFile,
@NonNull File binDir,
@NonNull byte[] bytes) {
super();
this.file = file;
this.jarFile = jarFile;
this.binDir = binDir;
this.bytes = bytes;
}
@NonNull
public String path() {
if (jarFile != null) {
return jarFile.getPath() + ':' + file.getPath();
} else {
return file.getPath();
}
}
@Override
public int compareTo(@NonNull ClassEntry other) {
String p1 = file.getPath();
String p2 = other.file.getPath();
int m1 = p1.length();
int m2 = p2.length();
if (m1 == m2 && p1.equals(p2)) {
return 0;
}
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();
}
/**
* Creates a list of class entries from the given class path.
*
* @param client the client to report errors to and to use to read files
* @param classPath the class path (directories and jar files) to scan
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassPath(
@NonNull LintClient client, @NonNull List<File> classPath, boolean sort) {
if (!classPath.isEmpty()) {
List<ClassEntry> libraryEntries = new ArrayList<>(64);
addEntries(client, libraryEntries, classPath);
if (sort) {
Collections.sort(libraryEntries);
}
return libraryEntries;
} else {
return Collections.emptyList();
}
}
/**
* Creates a list of class entries from the given class path and specific set of files within
* it.
*
* @param client the client to report errors to and to use to read files
* @param classFiles the specific set of class files to look for
* @param classFolders the list of class folders to look in (to determine the package root)
* @param sort if true, sort the results
* @return the list of class entries, never null.
*/
@NonNull
public static List<ClassEntry> fromClassFiles(
@NonNull LintClient client,
@NonNull List<File> classFiles,
@NonNull List<File> classFolders,
boolean sort) {
List<ClassEntry> entries = new ArrayList<>(classFiles.size());
if (!classFolders.isEmpty()) {
for (File file : classFiles) {
String path = file.getPath();
if (file.isFile() && path.endsWith(DOT_CLASS)) {
try {
byte[] bytes = client.readBytes(file);
for (File dir : classFolders) {
if (path.startsWith(dir.getPath())) {
entries.add(new ClassEntry(file, null /* jarFile*/, dir, bytes));
break;
}
}
} catch (IOException e) {
client.log(e, null);
}
}
}
if (sort && !entries.isEmpty()) {
Collections.sort(entries);
}
}
return entries;
}
/**
* Given a classpath, add all the class files found within the directories and inside jar files
*/
private static void addEntries(
@NonNull LintClient client,
@NonNull List<ClassEntry> entries,
@NonNull List<File> classPath) {
for (File classPathEntry : classPath) {
if (classPathEntry.getName().endsWith(DOT_JAR)) {
//noinspection UnnecessaryLocalVariable
File jarFile = classPathEntry;
if (!jarFile.exists()) {
continue;
}
try (ZipFile jar = new ZipFile(jarFile)) {
Enumeration<? extends ZipEntry> enumeration = jar.entries();
while (enumeration.hasMoreElements()) {
ZipEntry entry = enumeration.nextElement();
if (entry.getName().endsWith(DOT_CLASS)) {
try (InputStream is = jar.getInputStream(entry)) {
byte[] bytes = ByteStreams.toByteArray(is);
if (bytes != null) {
File file = new File(entry.getName());
entries.add(new ClassEntry(file, jarFile, jarFile, bytes));
}
} catch (Exception e) {
client.log(e, null);
}
}
}
} catch (IOException e) {
client.log(e, "Could not read jar file contents from %1$s", jarFile);
}
} else if (classPathEntry.isDirectory()) {
//noinspection UnnecessaryLocalVariable
File binDir = classPathEntry;
List<File> classFiles = new ArrayList<>();
addClassFiles(binDir, classFiles);
for (File file : classFiles) {
try {
byte[] bytes = client.readBytes(file);
entries.add(new ClassEntry(file, null /* jarFile*/, binDir, bytes));
} catch (IOException e) {
client.log(e, null);
}
}
} else {
client.log(null, "Ignoring class path entry %1$s", classPathEntry);
}
}
}
/** Adds in all the .class files found recursively in the given directory */
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);
}
}
}
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param libraryEntries the set of library entries to consult
* @param classEntries the set of class entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client,
@NonNull List<ClassEntry> libraryEntries,
@NonNull List<ClassEntry> classEntries) {
int size = libraryEntries.size() + classEntries.size();
Map<String, String> map = Maps.newHashMapWithExpectedSize(size);
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, libraryEntries);
addSuperClasses(client, visitor, classEntries);
return map;
}
/**
* Creates a super class map (from class to its super class) for the given set of entries
*
* @param client the client to report errors to and to use to access files
* @param entries the set of library entries to consult
* @return a map from name to super class internal names
*/
@NonNull
public static Map<String, String> createSuperClassMap(
@NonNull LintClient client, @NonNull List<ClassEntry> entries) {
Map<String, String> map = Maps.newHashMapWithExpectedSize(entries.size());
SuperclassVisitor visitor = new SuperclassVisitor(map);
addSuperClasses(client, visitor, entries);
return map;
}
/** Adds in all the super classes found for the given class entries into the given map */
private static void addSuperClasses(
@NonNull LintClient client,
@NonNull SuperclassVisitor visitor,
@NonNull 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) {
client.log(
null,
"Error processing %1$s: broken class file? (%2$s)",
entry.path(),
t.getMessage());
}
}
}
/** 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(ASM5);
mMap = map;
}
@Override
public void visit(
int version,
int access,
String name,
String signature,
String superName,
String[] interfaces) {
// Record super class in the map (but don't waste space on java.lang.Object)
if (superName != null && !"java/lang/Object".equals(superName)) {
mMap.put(name, superName);
}
}
}
}