/* * 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.google.devtools.treeshaker; import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Table.Cell; import com.google.common.io.Files; import com.google.devtools.j2objc.ast.CompilationUnit; import com.google.devtools.j2objc.file.RegularInputFile; import com.google.devtools.j2objc.util.CodeReferenceMap; import com.google.devtools.j2objc.util.ErrorUtil; import com.google.devtools.j2objc.util.FileUtil; import com.google.devtools.j2objc.util.Parser; import com.google.devtools.j2objc.util.ProGuardUsageParser; import com.google.devtools.j2objc.util.TranslationEnvironment; import com.google.devtools.treeshaker.ElementReferenceMapper.ReferenceNode; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.nio.charset.Charset; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A tool for finding unused code in a Java program. * * @author Priyank Malvania */ public class TreeShaker { private final Options options; private final com.google.devtools.j2objc.Options j2objcOptions; private TranslationEnvironment env = null; static { // Enable assertions in the tree shaker. ClassLoader loader = TreeShaker.class.getClassLoader(); if (loader != null) { loader.setPackageAssertionStatus(TreeShaker.class.getPackage().getName(), true); } } public TreeShaker(Options options) throws IOException { this.options = options; j2objcOptions = new com.google.devtools.j2objc.Options(); j2objcOptions.load(new String[] { "-sourcepath", Strings.nullToEmpty(options.getSourcepath()), "-classpath", Strings.nullToEmpty(options.getClasspath()), "-encoding", options.fileEncoding(), "-source", options.sourceVersion().flag() }); } private Parser createParser(Options options) throws IOException { Parser parser = Parser.newParser(j2objcOptions); parser.addSourcepathEntries(j2objcOptions.fileUtil().getSourcePathEntries()); parser.addClasspathEntries(Strings.nullToEmpty(options.getBootclasspath())); parser.addClasspathEntries(j2objcOptions.fileUtil().getClassPathEntries()); return parser; } private static void exitOnErrorsOrWarnings(boolean treatWarningsAsErrors) { int nErrors = ErrorUtil.errorCount(); int nWarnings = ErrorUtil.warningCount(); if (nWarnings > 0 || nErrors > 0) { if (nWarnings > 0) { if (treatWarningsAsErrors) { System.err.println("Treating warnings as errors."); System.err.println("Failed with " + nWarnings + " warnings:"); } else { System.err.println("TreeShaker ran with " + nWarnings + " warnings:"); } for (String warning : ErrorUtil.getWarningMessages()) { System.err.println(" warning: " + warning); } } if (nErrors > 0) { System.err.println("Failed with " + nErrors + " errors:"); for (String error : ErrorUtil.getErrorMessages()) { System.err.println(" error: " + error); } } if (treatWarningsAsErrors) { nErrors += nWarnings; } System.exit(nErrors); } } private void testFileExistence() { for (String filePath : options.getSourceFiles()) { File f = new File(filePath); if (!f.exists()) { ErrorUtil.error("File not found: " + filePath); } } } private File stripIncompatible(List<String> sourceFileNames, Parser parser) throws IOException { File strippedDir = null; for (int i = 0; i < sourceFileNames.size(); i++) { String fileName = sourceFileNames.get(i); RegularInputFile file = new RegularInputFile(fileName); String source = j2objcOptions.fileUtil().readFile(file); if (!source.contains("J2ObjCIncompatible")) { continue; } if (strippedDir == null) { strippedDir = Files.createTempDir(); parser.prependSourcepathEntry(strippedDir.getPath()); } Parser.ParseResult parseResult = parser.parseWithoutBindings(file, source); String qualifiedName = parseResult.mainTypeName(); parseResult.stripIncompatibleSource(); String relativePath = qualifiedName.replace('.', File.separatorChar) + ".java"; File strippedFile = new File(strippedDir, relativePath); Files.createParentDirs(strippedFile); Files.write( parseResult.getSource(), strippedFile, j2objcOptions.fileUtil().getCharset()); sourceFileNames.set(i, strippedFile.getPath()); } return strippedDir; } public CodeReferenceMap getUnusedCode(CodeReferenceMap inputRootSet) throws IOException { Parser parser = createParser(options); final HashMap<String, ReferenceNode> elementReferenceMap = new HashMap<>(); final Set<String> staticSet = new HashSet<>(); final HashMap<String, Set<String>> overrideMap = new HashMap<>(); List<String> sourceFiles = options.getSourceFiles(); File strippedDir = stripIncompatible(sourceFiles, parser); Parser.Handler handler = new Parser.Handler() { @Override public void handleParsedUnit(String path, CompilationUnit unit) { if (env == null) { env = unit.getEnv(); } else { //TODO(malvania): Assertion fails! Remove this once we're sure all env utils are the same. //assert(unit.getEnv() == env); } new ElementReferenceMapper(unit, elementReferenceMap, staticSet, overrideMap).run(); } }; parser.parseFiles(sourceFiles, handler, options.sourceVersion()); FileUtil.deleteTempDir(strippedDir); if (ErrorUtil.errorCount() > 0) { return null; } UnusedCodeTracker tracker = new UnusedCodeTracker(env, elementReferenceMap, staticSet, overrideMap); tracker.mapOverridingMethods(); tracker.markUsedElements(inputRootSet); CodeReferenceMap codeMap = tracker.buildTreeShakerMap(); return codeMap; } private static CodeReferenceMap loadRootSetMap(Options options) { return ProGuardUsageParser.parseDeadCodeFile(options.getPublicRootSetFile()); } public static void writeCodeReferenceMapInfo(BufferedWriter writer, CodeReferenceMap map) throws IOException { writer.write("Dead Classes:\n"); for (String clazz : map.getReferencedClasses()) { writer.write(clazz + "\n"); } //TODO(malvania): Add output formatting that can be easily read by the parser in translator. writer.write("Dead Methods:\n"); for (Cell<String, String, ImmutableSet<String>> cell : map.getReferencedMethods().cellSet()) { writer.write(cell.toString() + "\n"); } } public static void writeToFile(String fileName, CodeReferenceMap map) throws IOException { File file = new File(fileName); try { BufferedWriter writer = Files.newWriter(file, Charset.defaultCharset()); writeCodeReferenceMapInfo(writer, map); writer.close(); } catch (IOException e) { ErrorUtil.error(e.getMessage()); } } public static void main(String[] args) { if (args.length == 0) { Options.help(true); } boolean treatWarningsAsErrors = false; try { Options options = Options.parse(args); treatWarningsAsErrors = options.treatWarningsAsErrors(); TreeShaker finder = new TreeShaker(options); finder.testFileExistence(); exitOnErrorsOrWarnings(treatWarningsAsErrors); CodeReferenceMap unusedCodeMap = finder.getUnusedCode(loadRootSetMap(options)); writeToFile("tree-shaker-report.txt", unusedCodeMap); } catch (IOException e) { ErrorUtil.error(e.getMessage()); } exitOnErrorsOrWarnings(treatWarningsAsErrors); } }