Java tutorial
/* * 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.psi.extract; import com.android.annotations.NonNull; import com.android.annotations.Nullable; import com.android.tools.lint.detector.api.LintUtils; import com.android.utils.XmlUtils; import com.google.common.base.Charsets; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; import com.google.common.io.ByteStreams; import com.google.common.io.Closeables; import com.google.common.io.Files; import org.intellij.lang.annotations.Language; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.signature.SignatureVisitor; import org.objectweb.asm.tree.AbstractInsnNode; import org.objectweb.asm.tree.AnnotationNode; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.FieldInsnNode; import org.objectweb.asm.tree.FieldNode; import org.objectweb.asm.tree.InsnList; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.InvokeDynamicInsnNode; import org.objectweb.asm.tree.LdcInsnNode; import org.objectweb.asm.tree.MethodInsnNode; import org.objectweb.asm.tree.MethodNode; import org.objectweb.asm.tree.TypeInsnNode; import org.objectweb.asm.tree.VarInsnNode; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.URL; import java.net.URLClassLoader; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.ListIterator; import java.util.Map; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.jar.JarOutputStream; import java.util.zip.ZipEntry; import static com.android.SdkConstants.DOT_CLASS; import static com.android.SdkConstants.DOT_JAR; import static com.google.common.base.Charsets.UTF_8; import static java.io.File.separator; import static java.io.File.separatorChar; import static org.objectweb.asm.Opcodes.ASM5; // TODO: Attempt to load all classes! // Clean up method body rewriting! public class ExtractPsi { public static final String GROUP_ID = "com.android.tools.external.com-intellij"; public static final String ARTIFACT_ID = "uast"; /* * List of classes from the included set of packages that we want to explicitly * skip; typically because they're not directly related to the AST/code model * we want to expose to lint. When updating to new versions of PSI in the future, * you may find additional classes that show up in the signature files that you * may want to exlude here. */ private static final Set<String> SKIPPED_CLASSES = Sets.newHashSet( "com/intellij/psi/ClassFileViewProviderFactory", "com/intellij/psi/JavaPsiFacade", "com/intellij/psi/PsiParserFacade", "com/intellij/psi/SmartTypePointerManager", "com/intellij/psi/XmlElementFactory", "com/intellij/psi/XmlElementFactoryImpl", "com/intellij/psi/FileResolveScopeProvider", "com/intellij/psi/FileContextProvider", "com/intellij/psi/FileTypeFileViewProviders", "com/intellij/psi/FileViewProvider", "com/intellij/psi/FileViewProviderFactory", "com/intellij/psi/LanguageFileViewProviders", "com/intellij/psi/PsiElementFactory", "com/intellij/psi/PsiDocumentManager", "com/intellij/psi/PsiManager", "com/intellij/psi/SyntaxTraverser", "com/intellij/psi/SmartPointerManager", "com/intellij/psi/SmartPsiElementPointer", "com/intellij/psi/SmartPsiFileRange", "com/intellij/psi/SmartTypePointer", "com/intellij/psi/SmartTypePointerManager", "com/intellij/psi/IdentitySmartPointer", "com/intellij/psi/PsiElementFinder", "com/intellij/psi/NonClasspathClassFinder", "com/intellij/psi/JVMElementFactory", "com/intellij/psi/JVMElementFactoryProvider", "com/intellij/psi/ClassTypePointerFactory", "com/intellij/psi/JVMElementFactory", "com/intellij/psi/JVMElementFactoryProvider", "com/intellij/psi/JVMElementFactories", "com/intellij/psi/PsiFileFactory", "com/intellij/psi/JavaDirectoryService", "com/intellij/psi/JspPsiUtil", "com/intellij/psi/RefResolveService", "com/intellij/psi/SdkResolveScopeProvider", "com/intellij/psi/AbstractReparseTestCase", "com/intellij/psi/LanguageInjector", "com/intellij/psi/InjectedLanguagePlaces", "com/intellij/psi/CustomHighlighterTokenType", "com/intellij/psi/IntentionFilterOwner", "com/intellij/psi/LanguageFileViewProviders", "com/intellij/psi/PsiReferenceContributor", "com/intellij/psi/PsiReferenceProvider", "com/intellij/psi/PsiReferenceProviderBean", "com/intellij/psi/PsiReferenceRegistrar", "com/intellij/psi/PsiReferenceService", "com/intellij/psi/PsiReferenceService$Hints", "com/intellij/psi/HintedReferenceHost", "com/intellij/psi/PsiResolveHelper", "com/intellij/psi/ReferenceProviderType", "com/intellij/psi/PsiJavaCodeReferenceCodeFragment", "com/intellij/psi/PsiExpressionCodeFragment", "com/intellij/psi/JavaCodeFragment", "com/intellij/psi/JavaCodeFragmentFactory", "com/intellij/psi/PsiTypeCodeFragment", "com/intellij/psi/util/TypeConversionUtil", "com/intellij/psi/ClassFileViewProvider", "com/intellij/psi/CommonReferenceProviderTypes", "com/intellij/psi/PsiTreeChangeEvent", "com/intellij/psi/PsiTreeChangeAdapter", "com/intellij/psi/PsiTreeChangeListener", "com/intellij/psi/PsiDiamondTypeImpl", // this and various helper look useful but point to a lot of impl stuff "com/intellij/psi/PsiInferenceHelper", "com/intellij/psi/GenericsUtil", "com/intellij/psi/DelegatePsiTarget", "com/intellij/psi/RenameableDelegatePsiTarget", "com/intellij/psi/ManipulatableTarget", "com/intellij/psi/LambdaUtil", "com/intellij/psi/PsiMethodReferenceUtil", "com/intellij/psi/XmlElementVisitor", "com/intellij/psi/XmlRecursiveElementVisitor", "com/intellij/psi/XmlRecursiveElementWalkingVisitor", "com/intellij/psi/WeigherExtensionPoint", "com/intellij/psi/LanguageSubstitutor", "com/intellij/psi/LanguageSubstitutors", "com/intellij/psi/ElementManipulators", "com/intellij/psi/LanguageAnnotationSupport", "com/intellij/psi/ElementDescriptionProvider", "com/intellij/psi/ElementDescriptionLocation", "com/intellij/psi/ElementDescriptionUtil", "com/intellij/psi/WeighingService"); private static final Map<String, String> WIPED_METHOD_BODIES = Maps.newHashMap(); static { // (1) PsiElementVisitor#visitElement(PsiElement element) - // its current implementation is just // ProgressIndicatorProvider.checkCanceled(); // which we *don't* want (service lookup for that provider etc) // (2) Remove the constructor body of PsiDisjunctionType; remove the // myManager field, and the newDisjunctionType(final List<PsiType> types) // method. These don't work outside of WIPED_METHOD_BODIES.put("createException", "com/intellij/psi/util/PsiUtilCore$NullPsiElement()Lcom/intellij/psi/PsiInvalidElementAccessException;"); WIPED_METHOD_BODIES.put("<init>", "com/intellij/psi/PsiDisjunctionType(Ljava/util/List;Lcom/intellij/psi/PsiManager;)V"); WIPED_METHOD_BODIES.put("visitElement", "com/intellij/psi/PsiElementVisitor(Lcom/intellij/psi/PsiElement;)V"); // PsiTypeVisitor touches implementation stuff we don't have yet WIPED_METHOD_BODIES.put("visitLambdaExpressionType", "com/intellij/psi/PsiTypeVisitor(Lcom/intellij/psi/PsiLambdaExpressionType;)Ljava/lang/Object;"); WIPED_METHOD_BODIES.put("visitMethodReferenceType", "com/intellij/psi/PsiTypeVisitor(Lcom/intellij/psi/PsiMethodReferenceType;)Ljava/lang/Object;"); WIPED_METHOD_BODIES.put("<init>", "com/intellij/psi/PsiInvalidElementAccessException(Lcom/intellij/psi/PsiElement;)V"); WIPED_METHOD_BODIES.put("<init>", "com/intellij/psi/PsiInvalidElementAccessException(Lcom/intellij/psi/PsiElement;Ljava/lang/String;Ljava/lang/Throwable;)V"); WIPED_METHOD_BODIES.put("getValueIcon", "com/intellij/openapi/ui/TreeComboBox$TreeListCellRenderer(Ljava/lang/Object;I)Ljavax/swing/Icon;"); WIPED_METHOD_BODIES.put("getLeastUpperBound", "com/intellij/psi/PsiDisjunctionType()Lcom/intellij/psi/PsiType;"); WIPED_METHOD_BODIES.put("getCanonicalText", "com/intellij/psi/PsiReferenceBase()Ljava/lang/String;"); // Note: we'll also rename this when visiting the class to wipe out the WIPED_METHOD_BODIES.put("<init>", "com/intellij/psi/PsiDisjunctionType(Ljava/util/List;Lcom/intellij/psi/PsiManager;)V"); } private static final Map<String, String> SKIPPED_METHODS = Maps.newHashMap(); static { SKIPPED_METHODS.put("importClass", ""); SKIPPED_METHODS.put("getVirtualFile", ""); SKIPPED_METHODS.put("getContainingDirectory", ""); SKIPPED_METHODS.put("getTokenBeforeOperand", ""); SKIPPED_METHODS.put("getHierarchicalMethodSignature", ""); SKIPPED_METHODS.put("getSignature", ""); SKIPPED_METHODS.put("getDeclarationScope", ""); SKIPPED_METHODS.put("isReplaceEquivalent", ""); SKIPPED_METHODS.put("getPsiRoots", ""); SKIPPED_METHODS.put("getFileType", ""); SKIPPED_METHODS.put("findElementAt", ""); SKIPPED_METHODS.put("findReferenceAt", ""); SKIPPED_METHODS.put("subtreeChanged", ""); SKIPPED_METHODS.put("findSuperMethodSignaturesIncludingStatic", ""); SKIPPED_METHODS.put("containsClassNamed", ""); SKIPPED_METHODS.put("add", ""); SKIPPED_METHODS.put("addBefore", ""); SKIPPED_METHODS.put("addAfter", ""); SKIPPED_METHODS.put("checkAdd", ""); SKIPPED_METHODS.put("addAnnotation", ""); SKIPPED_METHODS.put("addRange", ""); SKIPPED_METHODS.put("addRangeBefore", ""); SKIPPED_METHODS.put("addRangeAfter", ""); SKIPPED_METHODS.put("copy", ""); SKIPPED_METHODS.put("delete", ""); SKIPPED_METHODS.put("deleteChildRange", ""); SKIPPED_METHODS.put("checkDelete", ""); SKIPPED_METHODS.put("checkSetName", ""); SKIPPED_METHODS.put("checkSetModifierProperty", ""); SKIPPED_METHODS.put("replace", ""); SKIPPED_METHODS.put("getCopyableUserData", ""); SKIPPED_METHODS.put("putCopyableUserData", ""); SKIPPED_METHODS.put("processDeclarations", ""); SKIPPED_METHODS.put("processChildren", ""); SKIPPED_METHODS.put("processVariants", ""); SKIPPED_METHODS.put("advancedResolve", ""); SKIPPED_METHODS.put("multiResolve", ""); SKIPPED_METHODS.put("normalizeDeclaration", ""); SKIPPED_METHODS.put("handleElementRename", ""); SKIPPED_METHODS.put("handleQualifiedNameChange", ""); SKIPPED_METHODS.put("occursInPackagePrefixes", ""); SKIPPED_METHODS.put("getProject", ""); SKIPPED_METHODS.put("isReferenceTo", ""); SKIPPED_METHODS.put("isEquivalentTo", ""); SKIPPED_METHODS.put("isConvertibleFrom", "(Lcom/intellij/psi/PsiType;)Z"); SKIPPED_METHODS.put("getTypeByName", "(Ljava/lang/String;Lcom/intellij/openapi/project/Project;Lcom/intellij/psi/search/GlobalSearchScope;)Lcom/intellij/psi/PsiClassType;"); SKIPPED_METHODS.put("getUseScope", ""); SKIPPED_METHODS.put("getResolveScope", ""); SKIPPED_METHODS.put("getVariants", ""); SKIPPED_METHODS.put("equals", "com/intellij/psi/PsiClassType"); SKIPPED_METHODS.put("getBoxedType", "com/intellij/psi/PsiPrimitiveType"); SKIPPED_METHODS.put("getViewProvider", ""); SKIPPED_METHODS.put("visitCodeFragment", "(Lcom/intellij/psi/JavaCodeFragment;)V"); SKIPPED_METHODS.put("hasNonTrivialParameters", "com/intellij/psi/PsiClassType()Z"); SKIPPED_METHODS.put("containsClassNamed", "com/intellij/psi/PsiPackage(Ljava/lang/String;)Z"); SKIPPED_METHODS.put("treeWalkUp", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/scope/PsiScopeProcessor;Lcom/intellij/psi/PsiElement;Lcom/intellij/psi/PsiElement;Lcom/intellij/psi/ResolveState;)Z"); SKIPPED_METHODS.put("mark", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/PsiElement;Ljava/lang/Object;)V"); SKIPPED_METHODS.put("findElementOfClassAtOffsetWithStopSet", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/PsiFile;ILjava/lang/Class;Z[Ljava/lang/Class;)Lcom/intellij/psi/PsiElement;"); SKIPPED_METHODS.put("releaseMark", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/PsiElement;Ljava/lang/Object;)Lcom/intellij/psi/PsiElement;"); SKIPPED_METHODS.put("decodeIndices", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/PsiElement;[Lcom/intellij/psi/PsiElement;)V"); SKIPPED_METHODS.put("copyElements", "com/intellij/psi/util/PsiTreeUtil([Lcom/intellij/psi/PsiElement;)[Lcom/intellij/psi/PsiElement;"); SKIPPED_METHODS.put("getInjectedElements", "com/intellij/psi/util/PsiTreeUtil(Lcom/intellij/psi/templateLanguages/OuterLanguageElement;)Ljava/util/List;"); SKIPPED_METHODS.put("findElementOfClassAtOffset", "com/intellij/psi/util/PsiTreeUtil"); SKIPPED_METHODS.put("findElementOfClassAtOffsetWithStopSet", "com/intellij/psi/util/PsiTreeUtil"); SKIPPED_METHODS.put("findElementOfClassAtRange", "com/intellij/psi/util/PsiTreeUtil"); SKIPPED_METHODS.put("isAssignableFrom", "com/intellij/psi/PsiType"); SKIPPED_METHODS.put("isConvertibleFrom", "com/intellij/psi/PsiType"); SKIPPED_METHODS.put("getDirectories", "com/intellij/psi/PsiDirectoryContainer"); SKIPPED_METHODS.put("getFiles", "com/intellij/psi/PsiPackage(Lcom/intellij/psi/search/GlobalSearchScope;)[Lcom/intellij/psi/PsiFile;"); SKIPPED_METHODS.put("findClassByShortName", "com/intellij/psi/PsiPackage(Ljava/lang/String;Lcom/intellij/psi/search/GlobalSearchScope;)[Lcom/intellij/psi/PsiClass;"); SKIPPED_METHODS.put("getForcedResolveScope", "com/intellij/psi/PsiCodeFragment()Lcom/intellij/psi/search/GlobalSearchScope;"); SKIPPED_METHODS.put("forceResolveScope", "com/intellij/psi/PsiCodeFragment(Lcom/intellij/psi/search/GlobalSearchScope;)V"); SKIPPED_METHODS.put("getManipulator", "com/intellij/psi/PsiReferenceBase()Lcom/intellij/psi/ElementManipulator;"); SKIPPED_METHODS.put("calculateDefaultRangeInElement", "com/intellij/psi/PsiReferenceBase()Lcom/intellij/openapi/util/TextRange;"); SKIPPED_METHODS.put("getRangeInElement", "com/intellij/psi/PsiReferenceBase()Lcom/intellij/openapi/util/TextRange;"); SKIPPED_METHODS.put("getValue", "com/intellij/psi/PsiReferenceBase()Ljava/lang/String;"); // Some additional methods with potentially generic sounding names, such as // getManager(), are specially filtered in isSkippedPsiMethod by also checking // the return type etc } private static final Set<String> REQUIRED_RESOURCES = Sets.newHashSet( // We have to include some resources from the IDE. I should prune this... "messages/JavaCoreBundle.properties"); private final File mInstalledIde; private final File mSourceDir; private final String mVersion; private final Map<String, ClassNode> mClassNodeMap = Maps.newHashMapWithExpectedSize(5000); private final Map<String, byte[]> mResourceMap = Maps.newHashMapWithExpectedSize(100); private Map<String, CgClass> mClassMap; private final Map<FieldNode, CgField> mFieldMap; private final Map<MethodNode, CgMethod> mMethodMap; public static final int MIN_MB = 2048; public static void main(String[] args) { try { if (Runtime.getRuntime().maxMemory() < MIN_MB / 2 * 1024 * 1024L) { System.err.println("Note: This analysis requires a lot of memory so make sure " + "you bump up the available space with this JVM flag: -Xmx" + MIN_MB + "m"); System.exit(-1); } if (args.length != 2) { System.err.println("Usage: " + ExtractPsi.class.getSimpleName() + " <studio-installation-root> <studio-source-tree>\n" + "studio-installation-root: directory with an install of Android Studio,\n" + " such as /Applications/Android Studio.app\n" + " studio-source-tree: repository checkout of Android Studio source code"); System.exit(-1); } File studioDir = new File(args[0]); File sourceDir = new File(args[1]); if (!studioDir.exists()) { System.err.println(studioDir + " does not exist"); System.exit(-1); } if (!sourceDir.exists()) { System.err.println(sourceDir + " does not exist"); System.exit(-1); } File toolsDir = new File(sourceDir, "tools"); if (!(toolsDir.isDirectory())) { System.out.println( sourceDir + " does not look like a Studio source tree; expected to find " + toolsDir); System.exit(-1); } File content = new File(studioDir, "Contents"); if (content.isDirectory()) { studioDir = content; } File lib = new File(studioDir, "lib"); if (!(lib.isDirectory())) { System.out .println(studioDir + " does not look like a Studio installation; expected to find " + lib); System.exit(-1); } String version = findVersionTag(lib); ExtractPsi extractor = new ExtractPsi(studioDir, sourceDir, version); extractor.perform(); } catch (Throwable t) { t.printStackTrace(); System.exit(-1); } } // Derive the version by looking up // lilb/resources.jar and reading in idea/AndroidStudioApplicationInfo.xml, // and then reading in component/build:apiVersion and stripping the AI- prefix // // This currently computes to "143.1821.5". private static String findVersionTag(File lib) { File resources = new File(lib, "resources.jar"); if (!resources.isFile()) { System.err.println(resources + " is not a file"); } try { String fileUrl = resources.toURI().toURL().toExternalForm(); URL url = new URL("jar:" + fileUrl + "!/idea/AndroidStudioApplicationInfo.xml"); InputStream stream = url.openStream(); assert stream != null; byte[] bytes = ByteStreams.toByteArray(stream); String xml = new String(bytes, Charsets.UTF_8); Document document = XmlUtils.parseDocumentSilently(xml, false); assert document != null; NodeList childNodes = document.getDocumentElement().getChildNodes(); for (int i = 0, n = childNodes.getLength(); i < n; i++) { Node node = childNodes.item(i); if (node.getNodeType() == Node.ELEMENT_NODE && "build".equals(node.getNodeName())) { Element element = (Element) node; String apiVersion = element.getAttribute("apiVersion"); assert apiVersion.startsWith("AI-") : apiVersion; return apiVersion.substring(3); } } } catch (Throwable e) { System.err.println("Failed to read/parse tag version: " + e); } System.err.println("Failed to find IDEA version number to use"); return null; } public ExtractPsi(@NonNull File installedIde, @NonNull File sourceDir, @NonNull String version) { mInstalledIde = installedIde; mSourceDir = sourceDir; mVersion = version; mClassMap = Maps.newHashMapWithExpectedSize(2000); mFieldMap = Maps.newHashMapWithExpectedSize(1000); mMethodMap = Maps.newHashMapWithExpectedSize(1000); } public void perform() { System.out.println("Performing analysis..."); try { List<File> jars = findJars(); // Read all into class maps readClasses(jars); System.out.println("Analyzing class graph for " + mClassNodeMap.values().size() + " classes"); } catch (IOException ioe) { System.err.println("Couldn't read in classes: " + ioe.getMessage()); System.exit(-1); } analyzeCallGraph(); try { writeSignatureFile(); writeMavenArtifact(); } catch (IOException ioe) { System.err.println("Couldn't write artifact: " + ioe.getMessage()); System.exit(-1); } } private void writeSignatureFile() { String report = recordSignatures(); try { File file = new File(mSourceDir, ("tools/base/misc/psi-extractor/src/main/java/com/android/" + "tools/lint/psi/extract/signatures.txt").replace('/', separatorChar)); if (!file.exists()) { file = File.createTempFile("report", ".txt"); } Files.write(report, file, UTF_8); System.out.println("Recorded API map in " + file); } catch (IOException ignore) { } } private static void writeCheckSumFiles(@NonNull File file) throws IOException { byte[] bytes = Files.toByteArray(file); Files.write(Hashing.md5().hashBytes(bytes).toString(), new File(file.getPath() + ".md5"), UTF_8); Files.write(Hashing.sha1().hashBytes(bytes).toString(), new File(file.getPath() + ".sha1"), UTF_8); } private void writeMavenArtifact() throws IOException { // Write artifact File maven = new File(mSourceDir, "prebuilts/tools/common/m2/repository".replace('/', separatorChar)); File artifactDir = new File(maven, GROUP_ID.replace('.', separatorChar) + separator + ARTIFACT_ID + separator + mVersion); if (!artifactDir.exists()) { boolean ok = artifactDir.mkdirs(); if (!ok) { System.err.println("Couldn't create artifact directory " + artifactDir); System.exit(-1); } } File metadaDir = artifactDir.getParentFile(); // Write NOTICE file File license = new File(mInstalledIde, "LICENSE.txt"); if (license.exists()) { try { Files.copy(license, new File(artifactDir, "NOTICE")); } catch (IOException e) { System.err.println("Couldn't copy license file " + license); System.exit(-1); } } String currentTime = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()); @Language("XML") String mavenMetadata = "" + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<metadata>\n" + " <groupId>" + GROUP_ID + "</groupId>\n" + " <artifactId>" + ARTIFACT_ID + "</artifactId>\n" + " <versioning>\n" + " <latest>" + mVersion + "</latest>\n" + " <release>" + mVersion + "</release>\n" + " <versions>\n" + " <version>" + mVersion + "</version>\n" + " </versions>\n" + " <lastUpdated>" + currentTime + "</lastUpdated>\n" + " </versioning>\n" + "</metadata>"; File mavenMetadataFile = new File(metadaDir, "maven-metadata.xml"); Files.write(mavenMetadata, mavenMetadataFile, UTF_8); writeCheckSumFiles(mavenMetadataFile); String baseName = ARTIFACT_ID + "-" + mVersion; @Language("XML") String pom = "" + "<project xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd\" xmlns:xsi=\"http\\\n" + "://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://maven.apache.org/POM/4.0.0\">\n" + " <modelVersion>4.0.0</modelVersion>\n" + " <groupId>" + GROUP_ID + "</groupId>\n" + " <artifactId>" + ARTIFACT_ID + "</artifactId>\n" + " <version>" + mVersion + "</version>\n" + " <packaging>jar</packaging>\n" + " <name>" + ARTIFACT_ID + "</name>\n" + " <url>http://www.jetbrains.com/idea</url>\n" + " <description>A subset of IntelliJ IDEA's AST APIs and implementation,\n" + " focusing on the read-only aspects, intended for use by Android Lint\n" + " when running outside of the IDE (typically from Gradle.)\n" + " </description>\n" + " <licenses>\n" + " <license>\n" + " <name>Apache License, Version 2.0</name>\n" + " <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>\n" + " </license>\n" + " </licenses>\n" + " <developers>\n" + " <developer>\n" + " <name>The Android Open Source Project</name>\n" + " </developer>\n" + " </developers>\n" + " <scm>\n" + " <connection>git://android.googlesource.com/platform/tools/base.git</connection>\n" + " <url>https://android.googlesource.com/platform/tools/base</url>\n" + " </scm>\n" + "</project>"; File pomFile = new File(artifactDir, baseName + ".pom"); Files.write(pom, pomFile, UTF_8); writeCheckSumFiles(pomFile); File jarFile = new File(artifactDir, baseName + DOT_JAR); writeJar(jarFile); writeCheckSumFiles(jarFile); createEmptyJars(new File(artifactDir, baseName + "-sources" + DOT_JAR)); createEmptyJars(new File(artifactDir, baseName + "-javadoc" + DOT_JAR)); testJar(jarFile); System.out.println( "Wrote artifact " + GROUP_ID + ":" + ARTIFACT_ID + ":" + mVersion + " to \n" + artifactDir); } private static void createEmptyJars(File outputFile) throws IOException { JarOutputStream jarOutputStream = new JarOutputStream( new BufferedOutputStream(new FileOutputStream(outputFile))); try { jarOutputStream.putNextEntry(new JarEntry("readme.txt")); try { jarOutputStream.write("checkout source code from git".getBytes()); } finally { jarOutputStream.closeEntry(); } } finally { jarOutputStream.close(); } writeCheckSumFiles(outputFile); } private void readClasses(@NonNull List<File> jars) throws IOException { for (File jar : jars) { JarInputStream zis = null; try { FileInputStream fis = new FileInputStream(jar); try { zis = new JarInputStream(fis); ZipEntry entry = zis.getNextEntry(); while (entry != null) { boolean directory = entry.isDirectory(); String name = entry.getName(); if (!directory && name.endsWith(DOT_CLASS)) { byte[] bytes = ByteStreams.toByteArray(zis); if (bytes != null) { ClassReader reader; ClassNode classNode; reader = new ClassReader(bytes); classNode = new ClassNode(); reader.accept(classNode, 0 /* flags */); // Classes shouldn't have been duplicated //assert !mClassNodeMap.containsKey(name) : name; mClassNodeMap.put(name, classNode); } } else if (!directory) { byte[] bytes = ByteStreams.toByteArray(zis); if (bytes != null) { mResourceMap.put(name, bytes); } } entry = zis.getNextEntry(); } } finally { Closeables.close(fis, true); } } finally { Closeables.close(zis, false); } } } @NonNull private List<File> findJars() { List<File> jars = Lists.newArrayList(); File root = mInstalledIde; File libs = new File(root, "lib"); jars.add(new File(libs, "openapi.jar")); jars.add(new File(libs, "util.jar")); jars.add(new File(libs, "trove4j.jar")); jars.add(new File(libs, "annotations.jar")); // Later: consider including *all* jars in the tree in the call graph analysis. // File[] files = libs.listFiles(); // if (files != null) { // for (File file : files) { // if (file.getPath().endsWith(DOT_JAR)) { // jars.add(file); // } // } // } return jars; } private void analyzeCallGraph() { Collection<ClassNode> classNodes = mClassNodeMap.values(); for (ClassNode classNode : classNodes) { mClassMap.put(classNode.name, new CgClass(classNode)); } for (CgClass clazz : mClassMap.values()) { clazz.recordDependencies(); } applyPsiReachability(); // First go and figure out which classes are *unreachable* by applying blacklisted // elements ArrayList<CgClass> classes = Lists.newArrayList(mClassMap.values()); for (CgClass clazz : classes) { if (clazz.isUnreachable()) { clazz.visitUnreachable(); } } for (CgClass clazz : classes) { if (clazz.isReachable()) { clazz.visitReachable(); } } // Any methods overriding an inherited method that is kept should also // be kept. Similarly, if a subclass method is reached, so should the // super class method! for (CgClass clazz : classes) { // First compute superclasses for (CgMethod method : clazz.mMethods.values()) { List<CgMethod> superMethods = method.getSuperMethods(); for (CgMethod superMethod : superMethods) { superMethod.addSubMethod(method); } } } while (true) { // Keep flowing values until we're not making progress mAddedReachableCount = 0; // Next apply reachability across the chains for (CgClass clazz : classes) { for (CgMethod method : clazz.mMethods.values()) { if (method.isSkipped()) { continue; } if ((method.isSubMethodReachable() || (method.mContainingClass.isReachable() && method.isSuperMethodReachable()))) { method.markSuperMethodsReachable(true); method.markSubMethodsReachable(true); } else if (/*method.mContainingClass.isReachable() &&*/ method.mContainingClass.extendsJdk()) { method.markSuperMethodsReachable(true); method.markSubMethodsReachable(true); } } } // Now we need to revisit in case there are further changes for (CgClass clazz : classes) { if (clazz.isReachable()) { clazz.visitReachable(); } } if (mAddedReachableCount == 0) { break; } } // Don't want to have to put icons on each PsiElement CgClass disjunctionType = findClass("com/intellij/psi/PsiDisjunctionType"); assert disjunctionType != null; // Mark DisjunctionType as abstract such that we can instantiate a subclass of it // Ensure that we include the constructor, even though it references PsiManager; we'll // wipe out that parameter in the bytecode writer (and we've already wiped its instruction // list) disjunctionType.mMethods.get("<init>(Ljava/util/List;Lcom/intellij/psi/PsiManager;)").markReachable(true, false, true); CgClass iconable = findClass("com/intellij/openapi/util/Iconable"); assert iconable != null && !iconable.mMethods.get("getIcon(I)").isReachable(); for (CgClass clazz : mClassMap.values()) { if (clazz.isReachable()) { clazz.pruneIncoming(); } } // Resources Collection<String> resourceNames = Lists.newArrayList(mResourceMap.keySet()); for (String name : resourceNames) { if (!REQUIRED_RESOURCES.contains(name)) { mResourceMap.remove(name); } } } private String recordSignatures() { StringBuilder sb = new StringBuilder(); List<CgClass> sortedClasses = Lists.newArrayList(); for (CgClass clazz : mClassMap.values()) { if (clazz.isReachable()) { sortedClasses.add(clazz); } } Collections.sort(sortedClasses); for (CgClass cls : sortedClasses) { List<CgMethod> sortedMethods = Lists.newArrayList(); for (CgMethod method : cls.mMethods.values()) { if (method.isReachable()) { sortedMethods.add(method); } } Collections.sort(sortedMethods); List<CgField> sortedFields = Lists.newArrayList(); for (CgField field : cls.mFields.values()) { if (field.isReachable()) { sortedFields.add(field); } } Collections.sort(sortedFields); sb.append(cls.getName()).append("\n"); for (CgField field : sortedFields) { sb.append(" ").append(field.mFieldNode.name).append("\n"); } for (CgMethod method : sortedMethods) { sb.append(" ").append(method.mMethodNode.name).append(method.mMethodNode.desc).append("\n"); } } return sb.toString(); } public void writeJar(File dest) throws IOException { if (dest.exists()) { boolean deleted = dest.delete(); if (!deleted) { throw new IOException("Could not delete " + dest); } } FileOutputStream fos = new FileOutputStream(dest); JarOutputStream zos = new JarOutputStream(fos); try { for (CgClass clazz : mClassMap.values()) { // TODO: Sort classes in the jar file? if (clazz.isReachable()) { byte[] bytes = computeClass(clazz); if (bytes != null) { String name = clazz.getName() + DOT_CLASS; JarEntry outEntry = new JarEntry(name); zos.putNextEntry(outEntry); zos.write(bytes); zos.closeEntry(); } } } // Resources for (String name : mResourceMap.keySet()) { byte[] bytes = mResourceMap.get(name); JarEntry outEntry = new JarEntry(name); zos.putNextEntry(outEntry); zos.write(bytes); zos.closeEntry(); } zos.flush(); } finally { Closeables.close(zos, false); } } private void testJar(File jar) { try { URL url = jar.toURI().toURL(); URLClassLoader loader = new URLClassLoader(new URL[] { url }, getClass().getClassLoader()); List<CgClass> verify = Lists.newArrayList(); //verify.addAll(mClassMap.values()); //Collections.sort(verify); verify.add(findClass("com/intellij/pom/java/LanguageLevel")); verify.add(findClass("com/intellij/psi/PsiClass")); verify.add(findClass("com/intellij/util/containers/ConcurrentWeakValueIntObjectHashMap")); // Check all reachable classes in the PSI package for (CgClass cls : mClassMap.values()) { if (cls.isReachable() && cls.getName().startsWith("com/intellij/psi/") && cls.getName().indexOf('/', "com/intellij/psi/".length()) == -1) { verify.add(cls); } } for (CgClass clazz : verify) { if (clazz.isReachable()) { //noinspection CaughtExceptionImmediatelyRethrown try { String name = clazz.getName(); name = name.replace('/', '.'); Class<?> aClass = loader.loadClass(name); aClass.getMethods(); aClass.getFields(); } catch (NoClassDefFoundError e) { System.err.println("While loading " + clazz.getName() + ": " + e); //throw e; } catch (VerifyError e) { System.err.println("While verifying " + clazz.getName() + ": " + e); //throw e; } } } } catch (Throwable t) { System.err.println("Invalid classes found in the generated jar: " + t); System.exit(-1); } } public byte[] computeClass(final CgClass clazz) throws IOException { assert clazz.isReachable(); ClassWriter classWriter = new ClassWriter(ASM5); ClassVisitor classVisitor = new ClassVisitor(ASM5, classWriter) { @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { CgClass clz = findClass(name); if (clz != null && clz.isReachable()) { super.visitInnerClass(name, outerName, innerName, access); } } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { // TODO: Filter out interfaces if they're not used? super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // Special cases: strip out method bodies for // (1) PsiElementVisitor#visitElement(PsiElement element) - // its current implementation is just // ProgressIndicatorProvider.checkCanceled(); // which we *don't* want (service lookup for that provider etc) // (2) Remove the constructor body of PsiDisjunctionType; remove the // myManager field, and the newDisjunctionType(final List<PsiType> types) // method. These don't work outside of // TODO: What about inner classes? Here the class name won't be right! CgMethod method = findMethod(clazz.getName(), name, desc); if (method != null && method.isReachable()) { if (method.toString().equals( "com/intellij/psi/PsiDisjunctionType#<init>(Ljava/util/List;Lcom/intellij/psi/PsiManager;)V")) { // Strip off the PsiManager parameter. The method body referencing it // has already been wiped out. desc = "(Ljava/util/List;)V"; signature = "(Ljava/util/List<Lcom/intellij/psi/PsiType;>;)V"; method.mMethodNode.invisibleParameterAnnotations = null; ArrayList<Object> parameters = new ArrayList<Object>(); parameters.add(method.mMethodNode.localVariables.get(0)); parameters.add(method.mMethodNode.localVariables.get(1)); method.mMethodNode.localVariables = parameters; } return super.visitMethod(access, name, desc, signature, exceptions); } return null; } @Override public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) { CgField method = findField(clazz.getName(), name); if (method != null && method.isReachable()) { return super.visitField(access, name, desc, signature, value); } return null; } }; clazz.mClassNode.accept(classVisitor); return classWriter.toByteArray(); } public void applyPsiReachability() { String pkgName = "com/intellij/psi/"; for (CgClass clazz : mClassMap.values()) { String owner = clazz.getName(); if (owner.startsWith(pkgName) && owner.indexOf('/', pkgName.length()) == -1) { if (clazz.isSkipped()) { clazz.markReachable(false, false, false); continue; } clazz.markReachable(true, true, false); } } pkgName = "com/intellij/psi/util/"; for (CgClass clazz : mClassMap.values()) { String name = clazz.getName(); if (name.startsWith(pkgName) && name.indexOf('/', pkgName.length()) == -1) { // Here we're only white-listing PsiTreeUtil for now if (name.equals("com/intellij/psi/util/PsiTreeUtil")) { clazz.markReachable(true, true, false); } } } // Include everything in javadoc pkgName = "com/intellij/psi/javadoc/"; for (CgClass clazz : mClassMap.values()) { String name = clazz.getName(); if (name.startsWith(pkgName) && name.indexOf('/', pkgName.length()) == -1) { clazz.markReachable(true, true, false); } } // We also need TextRange CgClass clazz = findClass("com/intellij/openapi/util/TextRange"); assert clazz != null; clazz.markReachable(true, true, false); // And Language Level constants clazz = findClass("com/intellij/pom/java/LanguageLevel"); assert clazz != null; clazz.markReachable(true, true, false); // ConcurrentHashMap performs some crazy reflection to access fields; whitelist these clazz = findClass("com/intellij/util/containers/ConcurrentHashMap"); assert clazz != null; clazz.markReachable(true, true, false); clazz = findClass("com/intellij/util/containers/ConcurrentLongObjectHashMap"); assert clazz != null; clazz.markReachable(true, true, false); clazz = findClass("com/intellij/util/containers/ConcurrentIntObjectHashMap"); assert clazz != null; clazz.markReachable(true, true, false); clazz = findClass("com/intellij/util/containers/ConcurrentWeakValueIntObjectHashMap"); assert clazz != null; clazz.markReachable(true, true, false); } @Nullable private CgClass findClass(@NonNull String owner) { return mClassMap.get(owner); } @Nullable private CgField findField(@NonNull String owner, @NonNull String name) { CgClass clazz = mClassMap.get(owner); if (clazz != null) { CgField field = clazz.mFields.get(name); if (field != null) { return field; } } return null; } @NonNull private static String getMethodHandle(@NonNull String name, @NonNull String desc) { StringBuilder sb = new StringBuilder(name.length() + desc.length()); sb.append(name); for (int i = 0, n = desc.length(); i < n; i++) { char c = desc.charAt(i); sb.append(c); if (c == ')') { break; } } return sb.toString(); } @Nullable private CgMethod findMethod(@NonNull String owner, @NonNull String name, @NonNull String desc) { CgClass clazz = mClassMap.get(owner); if (clazz != null) { CgMethod method = clazz.mMethods.get(getMethodHandle(name, desc)); if (method != null) { return method; } } return null; } /** Class Graph node */ private abstract class CgNode { /** TRUE: Yes, FALSE: No, null: Not yet considered */ public Boolean reachable; protected final List<CgNode> mIncoming = Lists.newArrayList(); protected final List<CgNode> mOutgoing = Lists.newArrayList(); @Override public abstract String toString(); protected void addDependency(CgNode to) { if (to == this) { // No need to point to "self" return; } mOutgoing.add(to); to.mIncoming.add(this); } public boolean isReachable() { return reachable == Boolean.TRUE; } public boolean isUnreachable() { // Not the same as !isReachable - null means "not yet visited" return reachable == Boolean.FALSE; } //private abstract boolean isSkipped(); protected void visitReachable() { assert isReachable(); // should only be called on reachable nodes for (CgNode node : mOutgoing) { if (node.reachable == null) { if (!node.isSkipped()) { node.markReachable(true, false, true); node.visitReachable(); } } if (node.reachable == Boolean.FALSE || node instanceof Member && ((Member) node).mContainingClass.reachable == Boolean.FALSE) { // Uh oh - reachable code calls code deliberately marked as non-reachable // We need to go and strip out String message = "Reachable code path (" + this + ") points " + "to blacklisted code (" + node + ")"; if (isWipedMethod()) { // It's okay to call a method where we've removed the implementation return; } if (message.equals( "Reachable code path (com/intellij/psi/util/PsiTreeUtil#getParentOfType(Lcom/intellij/psi/PsiElement;Ljava/lang/Class;ZI)Lcom/intellij/psi/PsiElement;) points to blacklisted code (com/intellij/psi/PsiElement#getNode()Lcom/intellij/lang/ASTNode;)")) { // Deliberately whitelisted: the getNode() path is usually not // called (but I should consider rewriting it to remove it instead) return; } throw new IllegalStateException(message); } } } protected boolean addTypeDependency(@Nullable Type type) { if (type == null) { return false; } if (type.getSort() == Type.ARRAY) { type = type.getElementType(); } if (type.getSort() != Type.OBJECT) { return false; } String internalName = type.getInternalName(); if (internalName.indexOf('/') != -1) { // object type? CgClass clz = findClass(internalName); if (clz != null) { addDependency(clz); return true; } } return false; } protected void addAnnotationDependency(@Nullable List list) { @SuppressWarnings("unchecked") List<AnnotationNode> annotations = (List<AnnotationNode>) list; if (annotations != null) { for (AnnotationNode annotation : annotations) { addAnnotationDependency(annotation); } } } public abstract boolean isSkipped(); public boolean isWipedMethod() { return false; } protected void addAnnotationDependency(AnnotationNode annotation) { addTypeDependency(Type.getType(annotation.desc)); if (annotation.values != null) { for (Object o : annotation.values) { if (o instanceof List) { for (Object o2 : (List) o) { if (o2 instanceof String[]) { String[] strings = (String[]) o2; if (strings.length == 2 && strings[0].startsWith("L") && strings[0].endsWith(";")) { addTypeDependency(Type.getType(strings[0])); } } } } else if (o instanceof String[]) { String[] strings = (String[]) o; if (strings.length == 2 && strings[0].startsWith("L") && strings[0].endsWith(";")) { addTypeDependency(Type.getType(strings[0])); } } } } } protected void visitUnreachable() { assert isUnreachable(); // should only be called on reachable nodes for (CgNode node : mIncoming) { if (node.reachable == null) { node.markReachable(false, false, false); node.visitUnreachable(); } if (node.reachable == Boolean.TRUE) { // Uh oh - unreachable code calls code deliberately marked as reachable // We need to go and figure out how to resolve this! String message = "Reachable code " + node + " points to blacklisted code " + this; if (node.isWipedMethod()) { return; } throw new IllegalStateException(message); } } } // Removes edges that aren't from reachable nodes void pruneIncoming() { ListIterator<CgNode> iterator = mIncoming.listIterator(); while (iterator.hasNext()) { CgNode edge = iterator.next(); if (edge.reachable == null) { iterator.remove(); } } } public void markReachable(boolean reachable, boolean includeChildren, boolean includeParents) { if (reachable && this.reachable == null) { mAddedReachableCount++; } this.reachable = reachable; } } private int mAddedReachableCount; private abstract class Member extends CgNode { protected final CgClass mContainingClass; public Member(CgClass containingClass) { mContainingClass = containingClass; } @Override public void markReachable(boolean reachable, boolean includeChildren, boolean includeParents) { super.markReachable(reachable, includeChildren, includeParents); // If a member is reachable, so is its parent class if (reachable && includeParents) { mContainingClass.markReachable(true, false, true); } } } private class CgField extends Member implements Comparable<CgField> { private final FieldNode mFieldNode; public CgField(CgClass clazz, FieldNode fieldNode) { super(clazz); mFieldNode = fieldNode; mFieldMap.put(fieldNode, this); } @Override public void markReachable(boolean reachable, boolean includeChildren, boolean includeParents) { assert !reachable || !isSkipped() : this; super.markReachable(reachable, includeChildren, includeParents); } public void recordDependencies() { if (mFieldNode.signature != null) { new SignatureReader(mFieldNode.signature).accept(new SignatureVisitor(ASM5) { @Override public void visitClassType(String name) { CgClass clazz = findClass(name); if (clazz != null) { addDependency(clazz); } super.visitClassType(name); } }); } String desc = mFieldNode.desc; addTypeDependency(Type.getReturnType(desc)); addAnnotationDependency(mFieldNode.visibleTypeAnnotations); addAnnotationDependency(mFieldNode.invisibleTypeAnnotations); addAnnotationDependency(mFieldNode.visibleAnnotations); addAnnotationDependency(mFieldNode.invisibleAnnotations); } @Override public boolean isSkipped() { String name = mFieldNode.name; String desc = mFieldNode.desc; String clsOwner = mContainingClass.getName(); // These types contain a LOT of implementation gunk; remove them for now // (except the accept method required for external visitor calls) // startsWith instead of equals: include inner classes too if (clsOwner.startsWith("com/intellij/psi/PsiWildcardType") || clsOwner.startsWith("com/intellij/psi/PsiIntersectionType") || clsOwner.startsWith("com/intellij/psi/PsiCapturedWildcardType") || clsOwner.startsWith("com/intellij/psi/PsiDisjunctionType")) { if (desc.equals("Lcom/intellij/psi/PsiManager;")) { return true; } if (name.equals("myLubCache")) { return true; } } return false; } @Override public String toString() { return mContainingClass.getName() + "#" + mFieldNode.name; } @Override public int compareTo(@NonNull CgField other) { int delta = mFieldNode.name.compareTo(other.mFieldNode.name); if (delta == 0) { delta = mFieldNode.desc.compareTo(other.mFieldNode.desc); } return delta; } } @SuppressWarnings("RedundantIfStatement") public class CgMethod extends Member implements Comparable<CgMethod> { private final MethodNode mMethodNode; private List<CgMethod> mSuperMethods; private List<CgMethod> mSubMethods; public CgMethod(CgClass containingClass, MethodNode methodNode) { super(containingClass); mMethodNode = methodNode; mMethodMap.put(methodNode, this); } @Override public boolean isWipedMethod() { String name = mMethodNode.name; String clsOwner = mContainingClass.getName(); if (clsOwner.equals("com/intellij/psi/PsiInvalidElementAccessException")) { // Remove most methods; this exception is important since it's included // in lots of signatures, but we'll never raise it (and it calls into ASTNode // via getNode() in the constructor.) return name.equals("<init>"); } return matches(WIPED_METHOD_BODIES); } protected boolean matches(Map<String, String> map) { String name = mMethodNode.name; String desc = mMethodNode.desc; String clsOwner = mContainingClass.getName(); String skipDesc = map.get(name); if (skipDesc != null) { if (skipDesc.isEmpty()) { // no desc in skip map means match all signatures // ...but only in the core packages if (clsOwner.startsWith("com/intellij/psi/") || clsOwner.startsWith("com/intellij/pom/")) { return true; } else { return false; } } if (!skipDesc.startsWith("(")) { // Owner String owner = skipDesc; int index = owner.indexOf('('); if (index != -1) { owner = owner.substring(0, index); } if (!owner.equals(clsOwner)) { return false; } if (index != -1) { skipDesc = skipDesc.substring(index); // Continue with parameter check } else { return true; } } if (skipDesc.equals(desc)) { return true; } else if (getMethodHandle(name, skipDesc).equals(getMethodHandle(name, desc))) { System.out.println("Only return type was different: verify that this is as expected"); } } return false; } private List<CgMethod> getSuperMethods() { if (mSuperMethods == null) { mSuperMethods = Lists.newArrayList(); String methodHandle = getMethodHandle(mMethodNode.name, mMethodNode.desc); addSuperMethods(mSuperMethods, methodHandle); } return mSuperMethods; } private void addSuperMethods(List<CgMethod> result, String methodHandle) { CgClass cls = mContainingClass.findSuperClass(); while (cls != null) { CgMethod method = cls.mMethods.get(methodHandle); if (method != null) { result.add(method); break; } cls = cls.findSuperClass(); } @SuppressWarnings("unchecked") // ASM API List<String> interfaceList = mContainingClass.mClassNode.interfaces; if (interfaceList != null) { for (String name : interfaceList) { cls = findClass(name); while (cls != null) { CgMethod method = cls.mMethods.get(methodHandle); if (method != null) { result.add(method); break; } cls = cls.findSuperClass(); } } } } // Return true if this method or any of its overriding/sub methods are reachable protected boolean isSubMethodReachable() { // Once you get to an overriding method in a class that isn't referenced, // you can ignore the methods (and any subclasses, which can't be reachable // either since otherwise this class would be.) if (!mContainingClass.isReachable() || isSkipped()) { return false; } if (isReachable()) { return true; } for (CgMethod sub : getSubMethods()) { if (sub.isSubMethodReachable()) { return true; } } return false; } protected boolean isSuperMethodReachable() { if (isReachable()) { return true; } for (CgMethod sup : getSuperMethods()) { if (sup.isSuperMethodReachable()) { return true; } } return false; } @Override public void markReachable(boolean reachable, boolean includeChildren, boolean includeParents) { assert !reachable || !isSkipped() // Special case handled by rewriting || "com/intellij/psi/PsiDisjunctionType#<init>(Ljava/util/List;Lcom/intellij/psi/PsiManager;)V" .equals(toString()) : this; super.markReachable(reachable, includeChildren, includeParents); } protected void markSubMethodsReachable(boolean reachable) { if (!mContainingClass.isReachable() || isSkipped()) { return; } markReachable(reachable, false, false); for (CgMethod sub : getSubMethods()) { sub.markSubMethodsReachable(reachable); } } protected void markSuperMethodsReachable(boolean reachable) { if (isSkipped()) { return; } markReachable(reachable, false, false); for (CgMethod sup : getSuperMethods()) { sup.markSuperMethodsReachable(reachable); } } public void addSubMethod(CgMethod method) { if (mSubMethods == null) { mSubMethods = Lists.newArrayList(); } mSubMethods.add(method); } public List<CgMethod> getSubMethods() { return mSubMethods == null ? Collections.<CgMethod>emptyList() : mSubMethods; } @Override public String toString() { return mContainingClass.getName() + "#" + mMethodNode.name + mMethodNode.desc; } @Override public boolean isSkipped() { String name = mMethodNode.name; String desc = mMethodNode.desc; String clsOwner = mContainingClass.getName(); if (name.equals("getInstance") && desc.startsWith("(Lcom/intellij/openapi/project/Project;)L")) { return true; } // Strip out methods from UserData and Iconable - don't want them on PsiElement if (name.equals("getUserData") || name.equals("putUserData")) { return true; } // Nothing particularly hard about this, but we don't model packages in lint // (and haven't had a need for it) so don't include methods that don't work if ((name.equals("getClasses") || name.equals("getSubPackages")) && clsOwner.equals("com/intellij/psi/PsiPackage")) { return true; } if (name.equals("getIcon") && (clsOwner.startsWith("com/intellij/psi/") || clsOwner.startsWith("com/intellij/pom/") || clsOwner.equals("com/intellij/openapi/util/Iconable") || clsOwner.equals("com/intellij/navigation/ItemPresentation"))) { return true; } if (name.equals("getPresentation") && (clsOwner.startsWith("com/intellij/psi/") || clsOwner.startsWith("com/intellij/pom/") || clsOwner.equals("com/intellij/navigation/NavigationItem"))) { return true; } // Skip setters etc - but only in PSI, not for example com/intellij/util/containers if (clsOwner.startsWith("com/intellij/psi/") || clsOwner.startsWith("com/intellij/pom/")) { if (name.startsWith("set") && desc.endsWith(")V")) { // Skip void setters return true; } if (name.startsWith("set") && name.length() >= 4 && Character.isUpperCase(name.charAt(3)) && !desc.contains("()")) { return true; } if (name.startsWith("bind") && name.length() >= 5 && Character.isUpperCase(name.charAt(4)) && !desc.contains("()")) { return true; } if (name.startsWith("handle") && name.length() >= 6 && Character.isUpperCase(name.charAt(5)) && !desc.contains("()")) { return true; } if (name.startsWith("getJavaLang") && desc.endsWith(")Lcom/intellij/psi/PsiClassType;")) { // getJavaLangThrowable, etc return true; } if (name.equals("getNode") && desc.startsWith("()") && desc.contains("ASTNode")) { // ASTNode, FileASTNode etc return true; } if ((name.equals("getManager") || name.equals("getPsiManager")) && desc.equals("()Lcom/intellij/psi/PsiManager;")) { return true; } if (name.equals("visitFile") && (clsOwner.equals("com/intellij/psi/PsiRecursiveElementWalkingVisitor") || clsOwner.equals("com/intellij/psi/PsiRecursiveElementVisitor"))) { // Just use super; these are looking up file providers etc return true; } if (clsOwner.equals("com/intellij/psi/PsiInvalidElementAccessException")) { // Remove most methods; this exception is important since it's included // in lots of signatures, but we'll never raise it (and it calls into ASTNode // via getNode() in the constructor.) return !name.equals("<init>"); } // These types contain a LOT of implementation gunk; remove them for now // (except the accept method required for external visitor calls) if (clsOwner.startsWith("com/intellij/psi/PsiWildcardType") || clsOwner.startsWith("com/intellij/psi/PsiIntersectionType") || clsOwner.startsWith("com/intellij/psi/PsiCapturedWildcardType")) { return !name.equals("accept") // PsiIntersectionType - needed by visitor && !name.equals("getConjuncts") // PsiCapturedWildType - needed by visitor && !name.equals("getWildcard"); } if (clsOwner.startsWith("com/intellij/psi/PsiDisjunctionType")) { if (name.equals("getLeastUpperBound") || name.equals("getPresentableText") || name.equals("getCanonicalText") || name.equals("getInternalCanonicalText") || name.equals("equalsToText") || name.equals("getSuperTypes") || name.equals("getDisjunctions") || name.equals("isValid") || name.equals("equals") || name.equals("hashCode") || name.equals("accept") // inner classes || (clsOwner.startsWith("com/intellij/psi/PsiDisjunctionType$") && (name.equals("<init>") || name.equals("fun")))) { // in inner classes // but we wipe the method impl; ecj must must customize // TODO: Mark the method as abstract? return false; } else { return true; } } } if (matches(SKIPPED_METHODS)) { return true; } return false; } public void recordDependencies() { // Include return type // using signature instead of desc to get type variables // e.g. for findMethodsAndTheirSubstitutorsByName we may have // desc=(Ljava/lang/String;Z)Ljava/util/List; // signature=(Ljava/lang/String;Z)Ljava/util/List<Lcom/intellij/openapi/util/Pair<Lcom/intellij/psi/PsiMethod;Lcom/intellij/psi/PsiSubstitutor;>;>; // and we want to include the types as well if (mMethodNode.signature != null) { new SignatureReader(mMethodNode.signature).accept(new SignatureVisitor(ASM5) { @Override public void visitClassType(String name) { CgClass clazz = findClass(name); if (clazz != null) { addDependency(clazz); } super.visitClassType(name); } @Override public void visitInnerClassType(String name) { CgClass clazz = findClass(name); if (clazz != null) { addDependency(clazz); } super.visitInnerClassType(name); } }); } String desc = mMethodNode.desc; Type[] argumentTypes = Type.getArgumentTypes(desc); for (Type type : argumentTypes) { addTypeDependency(type); } addTypeDependency(Type.getReturnType(desc)); if (isWipedMethod()) { // TODO: Move this to the WIPED_METHODs list: put replacement code // there! if (mMethodNode.name.equals("<init>") && mContainingClass.getName().equals("com/intellij/psi/PsiInvalidElementAccessException")) { mMethodNode.instructions.clear(); mMethodNode.tryCatchBlocks = null; mMethodNode.instructions.add(new InsnNode(Opcodes.RETURN)); } else if (mMethodNode.name.equals("<init>") && mContainingClass.getName().equals("com/intellij/psi/PsiDisjunctionType")) { mMethodNode.instructions.clear(); mMethodNode.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); mMethodNode.instructions.add(new FieldInsnNode(Opcodes.GETSTATIC, "com/intellij/psi/PsiAnnotation", "EMPTY_ARRAY", "[Lcom/intellij/psi/PsiAnnotation;")); mMethodNode.instructions .add(new MethodInsnNode(Opcodes.INVOKESPECIAL, "com/intellij/psi/PsiType$Stub", "<init>", "([Lcom/intellij/psi/PsiAnnotation;)V", false)); mMethodNode.instructions.add(new VarInsnNode(Opcodes.ALOAD, 0)); mMethodNode.instructions.add(new VarInsnNode(Opcodes.ALOAD, 1)); mMethodNode.instructions.add(new FieldInsnNode(Opcodes.PUTFIELD, "com/intellij/psi/PsiDisjunctionType", "myTypes", "Ljava/util/List;")); mMethodNode.instructions.add(new InsnNode(Opcodes.RETURN)); } else { mMethodNode.instructions.clear(); if (desc.endsWith(")V")) { mMethodNode.instructions.add(new InsnNode(Opcodes.RETURN)); } else if (desc.endsWith(";")) { mMethodNode.instructions.add(new InsnNode(Opcodes.ACONST_NULL)); mMethodNode.instructions.add(new InsnNode(Opcodes.ARETURN)); } else { // Need to handle this scenario: primitive value return throw new IllegalArgumentException(desc); } } } else { InsnList nodes = mMethodNode.instructions; for (int i = 0, n = nodes.size(); i < n; i++) { AbstractInsnNode instruction = nodes.get(i); if (instruction instanceof InvokeDynamicInsnNode) { System.out.println("Warning: Ignoring invokeDynamic"); } else if (instruction instanceof MethodInsnNode) { MethodInsnNode node = (MethodInsnNode) instruction; CgMethod method = findMethod(node.owner, node.name, node.desc); if (method != null) { addDependency(method); } else if (node.owner.equals("java/lang/Class") && (node.name.equals("getDeclaredField") || node.name.equals("getField") || node.name.equals("getMethod") || node.name.equals("getDeclaredMethod") || node.name.equals("forName"))) { boolean ok = false; String containingClass = mContainingClass.getName(); if (containingClass.equals( "com/intellij/psi/impl/ConstantExpressionVisitor#visitReferenceExpression(Lcom/intellij/psi/PsiReferenceExpression;)V") || containingClass.equals("com/intellij/util/containers/ConcurrentIntHashMap") || containingClass.equals("com/intellij/util/containers/ConcurrentHashMap") || containingClass .equals("com/intellij/util/containers/ConcurrentLongObjectHashMap") || containingClass .equals("com/intellij/util/containers/ConcurrentIntObjectHashMap")) { // already handled ok = true; } else { AbstractInsnNode prev = LintUtils.getPrevInstruction(node); if (prev instanceof LdcInsnNode) { AbstractInsnNode prev2 = LintUtils.getPrevInstruction(prev); if (prev2 instanceof LdcInsnNode) { LdcInsnNode c = (LdcInsnNode) prev2; if (c.cst instanceof Type) { if (addTypeDependency((Type) c.cst)) { // Potentially check for method/field here System.out.println("TODO: Reflection access found for type " + c.cst + ": Also look for method/field references on that type?"); } else { ok = true; // Referring to some other system class } } } } } // Manually verified: these reflection accesses do not affect PSI call graph: if (!ok && (containingClass .equals("com/intellij/openapi/options/BeanConfigurable$BeanField") || containingClass.equals("com/intellij/util/lang/UrlClassLoader") || containingClass.equals("com/intellij/openapi/util/DefaultJDOMExternalizer") || containingClass.equals("com/intellij/openapi/util/io/FileUtil") || containingClass.startsWith("com/intellij/util/ui/UIUtil") || containingClass.equals("com/intellij/openapi/ui/popup/util/PopupUtil") || containingClass.equals("com/intellij/ide/ClassUtilCore") || containingClass .equals("com/intellij/openapi/diagnostic/LoggerRt$IdeaFactory") || containingClass .startsWith("com/intellij/util/io/PagedFileStorage$StorageLock") || containingClass .equals("com/intellij/openapi/util/JDOMExternalizableStringList") || containingClass.equals("com/intellij/util/InstanceofCheckerGenerator") || containingClass.startsWith("com/intellij/openapi/util/io/FileUtilRt") || containingClass.startsWith("com/intellij/openapi/util/io/FileSystemUtil") || containingClass.equals( "com/intellij/codeInsight/completion/CompletionAutoPopupTestCase") || containingClass.equals("com/intellij/openapi/ui/FixedComboBoxEditor") || containingClass.equals("com/intellij/openapi/projectRoots/JdkUtil") || containingClass.equals("com/intellij/openapi/application/ModalityState") || containingClass.equals("com/intellij/openapi/components/ServiceBean") || containingClass.equals("com/intellij/util/MemoryDumpHelper") || containingClass.equals("com/intellij/openapi/MnemonicHelper") || containingClass .equals("com/intellij/ide/ui/PublicMethodBasedOptionDescription") || containingClass.equals("com/intellij/openapi/MnemonicWrapper") || containingClass.equals("com/intellij/execution/rmi/RemoteUtil") || containingClass.equals("com/intellij/openapi/ui/DialogWrapperPeerFactory") || containingClass.equals( "com/intellij/openapi/externalSystem/util/ExternalSystemApiUtil") || containingClass.equals("com/intellij/openapi/util/IconLoader") || containingClass.equals("com/intellij/openapi/ui/MasterDetailsComponent") || containingClass.startsWith("com/intellij/ui/SearchTextField$1") || containingClass.startsWith("com/intellij/openapi/ui/Messages$MessageDialog") || containingClass.equals("com/intellij/lang/cacheBuilder/CacheBuilderEP") || containingClass.equals("com/intellij/openapi/module/ModuleType") || containingClass.equals("com/intellij/openapi/module/StdModuleTypes") || containingClass .equals("com/intellij/ide/ui/PublicFieldBasedOptionDescription") || containingClass .equals("com/intellij/openapi/externalSystem/model/DataNode$1") || containingClass.equals("com/intellij/ui/mac/foundation/MacUtil") || containingClass .equals("com/intellij/codeInspection/ui/SingleIntegerFieldOptionsPanel") || containingClass.startsWith("com/intellij/openapi/extensions/") || containingClass.startsWith("com/intellij/util/containers/Concurrent") || containingClass.equals("com/intellij/util/ReflectionUtil"))) { ok = true; } if (!ok) { System.out.println("WARNING: REFLECTION found in " + this + ": check for potentially missed code"); } } } else if (instruction instanceof FieldInsnNode) { FieldInsnNode node = (FieldInsnNode) instruction; CgField field = findField(node.owner, node.name); if (field != null) { addDependency(field); } } else if (instruction instanceof TypeInsnNode) { TypeInsnNode node = (TypeInsnNode) instruction; if (node.desc.endsWith(";")) { Type type = Type.getType(node.desc); if (type != null) { CgClass clazz = findClass(type.getInternalName()); if (clazz != null) { addDependency(clazz); } } } else if (node.desc.contains("/")) { CgClass clazz = findClass(node.desc); if (clazz != null) { addDependency(clazz); } } } else if (instruction instanceof LdcInsnNode) { LdcInsnNode ldc = (LdcInsnNode) instruction; if (ldc.cst instanceof Type) { CgClass clazz = findClass(((Type) ldc.cst).getInternalName()); if (clazz != null) { addDependency(clazz); } } } } } //noinspection unchecked List<String> exceptions = mMethodNode.exceptions; for (String s : exceptions) { CgClass clazz = findClass(s); if (clazz != null) { addDependency(clazz); } } addAnnotationDependency(mMethodNode.visibleTypeAnnotations); addAnnotationDependency(mMethodNode.invisibleTypeAnnotations); addAnnotationDependency(mMethodNode.visibleAnnotations); addAnnotationDependency(mMethodNode.invisibleAnnotations); } @Override public int compareTo(@NonNull CgMethod c2) { int delta = mMethodNode.name.compareTo(c2.mMethodNode.name); if (delta == 0) { delta = mMethodNode.desc.compareTo(c2.mMethodNode.desc); } return delta; } } public class CgClass extends CgNode implements Comparable<CgClass> { private final Map<String, CgMethod> mMethods = Maps.newHashMap(); private final Map<String, CgField> mFields = Maps.newHashMap(); private final ClassNode mClassNode; public boolean isInterface() { return (mClassNode.access & Opcodes.ACC_INTERFACE) != 0; } public boolean isAnnotation() { return (mClassNode.access & Opcodes.ACC_ANNOTATION) != 0; } @NonNull public String getName() { return mClassNode.name; } public boolean extendsJdk() { // Returns true if this class extends a JDK class (other than java.lang.Object) CgClass superCls = findSuperClass(); if (superCls != null && superCls.extendsJdk()) { return true; } if (isInterestingJdkClass(mClassNode.superName)) { return true; } if (mClassNode.interfaces != null) { @SuppressWarnings("unchecked") // ASM API List<String> interfaceList = mClassNode.interfaces; for (String signature : interfaceList) { CgClass clazz = findClass(signature); if (clazz != null && clazz.extendsJdk()) { return true; } if (isInterestingJdkClass(signature)) { return true; } } } return false; } private boolean isInterestingJdkClass(@Nullable String owner) { return owner != null && (owner.startsWith("java/") || owner.startsWith("javax/")) && !owner.equals("java/lang/Object"); } @Nullable public CgClass findSuperClass() { if (mClassNode.superName != null) { return findClass(mClassNode.superName); } return null; } public CgClass(ClassNode classNode) { mClassNode = classNode; @SuppressWarnings("rawtypes") // ASM API List fieldList = classNode.fields; for (Object f : fieldList) { FieldNode fieldNode = (FieldNode) f; CgField field = new CgField(this, fieldNode); mFields.put(fieldNode.name, field); } @SuppressWarnings("rawtypes") // ASM API List methodList = classNode.methods; for (Object f : methodList) { MethodNode methodNode = (MethodNode) f; CgMethod method = new CgMethod(this, methodNode); mMethods.put(getMethodHandle(methodNode.name, methodNode.desc), method); } } @Override protected void visitReachable() { super.visitReachable(); for (CgField field : mFields.values()) { if (field.isReachable()) { field.visitReachable(); } } for (CgMethod method : mMethods.values()) { if (method.isSkipped()) { continue; } if (isInterface() | isAnnotation()) { // Interface methods need to all be included if the interface is included // Also include all annotation methods method.markReachable(true, false, false); } else if (method.mMethodNode.name.equals("<clinit>")) { // Class initializers must be included if a class is! method.markReachable(true, false, false); } if (method.isReachable()) { method.visitReachable(); } } } @Override protected void visitUnreachable() { super.visitUnreachable(); for (CgField field : mFields.values()) { if (field.isUnreachable()) { field.visitUnreachable(); } } for (CgMethod method : mMethods.values()) { if (method.isUnreachable()) { method.visitUnreachable(); } } } @Override public boolean isSkipped() { String name = getName(); if (SKIPPED_CLASSES.contains(name)) { return true; } int index = name.indexOf('$'); return index != -1 && SKIPPED_CLASSES.contains(name.substring(0, index)); } @Override public void markReachable(boolean reachable, boolean includeChildren, boolean includeParents) { assert !reachable || !isSkipped() : this; super.markReachable(reachable, includeChildren, includeParents); // If a class is reachable, so are all its outer classes if (reachable && includeParents) { String outerClass = mClassNode.outerClass; if (outerClass != null) { CgClass clz = findClass(outerClass); if (clz != null) { clz.markReachable(true, false, true); // recursively } } } if (includeChildren) { for (CgField field : mFields.values()) { if (field.isSkipped()) { field.markReachable(false, true, false); continue; } field.markReachable(reachable, true, false); } for (CgMethod method : mMethods.values()) { // White-list all methods, except those specifically prohibited if (method.isSkipped()) { method.markReachable(false, true, false); continue; } method.markReachable(reachable, true, false); } } // What about inner classes? } public void recordDependencies() { for (CgField field : mFields.values()) { field.recordDependencies(); } // TODO: Specially handled field initializations? for (CgMethod method : mMethods.values()) { method.recordDependencies(); } if (mClassNode.signature != null) { new SignatureReader(mClassNode.signature).accept(new SignatureVisitor(ASM5) { @Override public void visitClassType(String name) { CgClass clazz = findClass(name); if (clazz != null) { addDependency(clazz); } super.visitClassType(name); } @Override public void visitInnerClassType(String name) { CgClass clazz = findClass(name); if (clazz != null) { addDependency(clazz); } super.visitInnerClassType(name); } }); } if (mClassNode.superName != null) { CgClass clazz = findClass(mClassNode.superName); if (clazz != null) { addDependency(clazz); } } if (mClassNode.interfaces != null) { @SuppressWarnings("unchecked") // ASM API List<String> interfaceList = mClassNode.interfaces; for (String signature : interfaceList) { CgClass clazz = findClass(signature); if (clazz != null) { addDependency(clazz); } } } addAnnotationDependency(mClassNode.visibleTypeAnnotations); addAnnotationDependency(mClassNode.invisibleTypeAnnotations); addAnnotationDependency(mClassNode.visibleAnnotations); addAnnotationDependency(mClassNode.invisibleAnnotations); } @Override public String toString() { return getName(); } @Override public int compareTo(@NonNull CgClass other) { return getName().compareTo(other.getName()); } } }