/* * 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.cyclefinder; import com.google.common.base.Strings; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; 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.translate.LambdaTypeElementAdder; import com.google.devtools.j2objc.translate.OuterReferenceResolver; import com.google.devtools.j2objc.util.ErrorUtil; import com.google.devtools.j2objc.util.FileUtil; import com.google.devtools.j2objc.util.Parser; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; /** * A tool for finding possible reference cycles in a Java program. * * @author Keith Stanger */ public class CycleFinder { private final Options options; private final com.google.devtools.j2objc.Options j2objcOptions; private final NameList blacklist; private final List<List<Edge>> cycles = new ArrayList<>(); private ReferenceGraph referenceGraph = null; static { // Enable assertions in the cycle finder. ClassLoader loader = CycleFinder.class.getClassLoader(); if (loader != null) { loader.setPackageAssertionStatus(CycleFinder.class.getPackage().getName(), true); } } public CycleFinder(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() }); blacklist = getBlacklist(); } private Parser createParser() { Parser parser = Parser.newParser(j2objcOptions); parser.addSourcepathEntries(Strings.nullToEmpty(options.getSourcepath())); parser.addClasspathEntries(Strings.nullToEmpty(options.getBootclasspath())); parser.addClasspathEntries(Strings.nullToEmpty(options.getClasspath())); return parser; } private static void exitOnErrors() { int nErrors = ErrorUtil.errorCount(); if (nErrors > 0) { System.err.println("Failed with " + nErrors + " errors:"); for (String error : ErrorUtil.getErrorMessages()) { System.err.println("error: " + error); } 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 NameList getBlacklist() throws IOException { List<String> blackListFiles = options.getBlacklistFiles(); if (blackListFiles.isEmpty()) { return null; } return NameList.createFromFiles(blackListFiles, options.fileEncoding()); } 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, Charset.forName(options.fileEncoding())); sourceFileNames.set(i, strippedFile.getPath()); } return strippedDir; } public void constructGraph() throws IOException { Parser parser = createParser(); NameList whitelist = NameList.createFromFiles(options.getWhitelistFiles(), options.fileEncoding()); final GraphBuilder graphBuilder = new GraphBuilder(whitelist); List<String> sourceFiles = options.getSourceFiles(); File strippedDir = stripIncompatible(sourceFiles, parser); Parser.Handler handler = new Parser.Handler() { @Override public void handleParsedUnit(String path, CompilationUnit unit) { new LambdaTypeElementAdder(unit).run(); new OuterReferenceResolver(unit).run(); graphBuilder.visitAST(unit); } }; parser.parseFiles(sourceFiles, handler, options.sourceVersion()); FileUtil.deleteTempDir(strippedDir); if (ErrorUtil.errorCount() > 0) { return; } // Construct the graph. referenceGraph = graphBuilder.constructGraph().getGraph(); } public List<List<Edge>> findCycles() { for (ReferenceGraph component : referenceGraph.getStronglyConnectedComponents(getSeedNodes(referenceGraph))) { handleStronglyConnectedComponent(component); } return cycles; } private Set<TypeNode> getSeedNodes(ReferenceGraph graph) { if (blacklist == null) { return graph.getNodes(); } Set<TypeNode> seedNodes = new HashSet<>(); for (TypeNode node : graph.getNodes()) { if (blacklist.containsType(node)) { seedNodes.add(node); } } return seedNodes; } private void handleStronglyConnectedComponent(ReferenceGraph subgraph) { // Make sure to find at least one cycle for each type in the SCC. Set<TypeNode> unusedTypes = Sets.newHashSet(subgraph.getNodes()); while (!unusedTypes.isEmpty()) { TypeNode root = Iterables.getFirst(unusedTypes, null); assert root != null; List<Edge> cycle = subgraph.findShortestCycle(root); if (shouldAddCycle(cycle)) { cycles.add(cycle); } for (Edge e : cycle) { unusedTypes.remove(e.getOrigin()); } } } public ReferenceGraph getReferenceGraph() { return referenceGraph; } private boolean shouldAddCycle(List<Edge> cycle) { if (blacklist == null) { return true; } for (Edge e : cycle) { if (blacklist.containsType(e.getOrigin())) { return true; } } return false; } public static void printCycles(Collection<? extends Iterable<Edge>> cycles, PrintStream out) { for (Iterable<Edge> cycle : cycles) { out.println(); out.println("***** Found reference cycle *****"); for (Edge e : cycle) { out.println(e.toString()); } out.println("----- Full Types -----"); for (Edge e : cycle) { out.println(e.getOrigin().getSignature()); } } out.println(); out.println(cycles.size() + " CYCLES FOUND."); } public static void main(String[] args) throws IOException { if (args.length == 0) { Options.help(true); } Options options = Options.parse(args); CycleFinder finder = new CycleFinder(options); finder.testFileExistence(); exitOnErrors(); finder.constructGraph(); exitOnErrors(); if (options.printReferenceGraph()) { finder.getReferenceGraph().print(System.out); } else { List<List<Edge>> cycles = finder.findCycles(); printCycles(cycles, System.out); System.exit(ErrorUtil.errorCount() + cycles.size()); } } }