diff --git a/src/main/java/pascal/taie/Main.java b/src/main/java/pascal/taie/Main.java index f091ae509..b488fa217 100644 --- a/src/main/java/pascal/taie/Main.java +++ b/src/main/java/pascal/taie/Main.java @@ -58,7 +58,7 @@ public static void main(String... args) { logger.info("No analyses are specified"); System.exit(0); } - buildWorld(options, plan.analyses()); + buildWorld(options); executePlan(plan); }, "Tai-e"); LoggerConfigs.reconfigure(); @@ -117,12 +117,11 @@ private static Plan processConfigs(Options options) { public static void buildWorld(String... args) { Options options = Options.parse(args); LoggerConfigs.setOutput(options.getOutputDir()); - Plan plan = processConfigs(options); - buildWorld(options, plan.analyses()); + buildWorld(options); LoggerConfigs.reconfigure(); } - private static void buildWorld(Options options, List analyses) { + private static void buildWorld(Options options) { Monitor.runAndCount(() -> { try { Class builderClass = options.getWorldBuilderClass(); @@ -131,7 +130,7 @@ private static void buildWorld(Options options, List analyses) { if (options.isWorldCacheMode()) { builder = new CachedWorldBuilder(builder); } - builder.build(options, analyses); + builder.build(options); logger.info("{} classes with {} methods in the world", World.get() .getClassHierarchy() @@ -143,7 +142,7 @@ private static void buildWorld(Options options, List analyses) { .mapToInt(c -> c.getDeclaredMethods().size()) .sum()); } catch (InstantiationException | IllegalAccessException | - NoSuchMethodException | InvocationTargetException e) { + NoSuchMethodException | InvocationTargetException e) { System.err.println("Failed to build world due to " + e); System.exit(1); } diff --git a/src/main/java/pascal/taie/WorldBuilder.java b/src/main/java/pascal/taie/WorldBuilder.java index 7236c325d..840ac8bb6 100644 --- a/src/main/java/pascal/taie/WorldBuilder.java +++ b/src/main/java/pascal/taie/WorldBuilder.java @@ -22,11 +22,8 @@ package pascal.taie; -import pascal.taie.config.AnalysisConfig; import pascal.taie.config.Options; -import java.util.List; - /** * Interface for {@link World} builder. */ @@ -35,10 +32,8 @@ public interface WorldBuilder { /** * Builds a new instance of {@link World} and make it globally accessible * through static methods of {@link World}. - * TODO: remove {@code analyses}. * - * @param options the options - * @param analyses the analyses to be executed + * @param options the options */ - void build(Options options, List analyses); + void build(Options options); } diff --git a/src/main/java/pascal/taie/config/Options.java b/src/main/java/pascal/taie/config/Options.java index add8a63b7..5cd412496 100644 --- a/src/main/java/pascal/taie/config/Options.java +++ b/src/main/java/pascal/taie/config/Options.java @@ -25,7 +25,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,6 +39,9 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import pascal.taie.WorldBuilder; +import pascal.taie.analysis.pta.PointerAnalysis; +import pascal.taie.analysis.pta.plugin.reflection.LogItem; +import pascal.taie.language.classes.StringReps; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @@ -48,10 +53,12 @@ import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.StringJoiner; import java.util.function.Predicate; /** @@ -261,9 +268,9 @@ public File getPlanFile() { description = "Analyses to be executed", paramLabel = "]>", mapFallbackValue = "") - private Map analyses = Map.of(); + private Map analyses = Map.of(); - public Map getAnalyses() { + public Map getAnalyses() { return analyses; } @@ -293,7 +300,9 @@ public Set getKeepResult() { * Parses arguments and return the parsed and post-processed Options. */ public static Options parse(String... args) { - Options options = CommandLine.populateCommand(new Options(), args); + CommandLine commandLine = new CommandLine(new Options()) + .registerConverter(AnalysisOptions.class, new AnalysisOptionsConverter()); + Options options = (Options) commandLine.parseArgs(args).commandSpec().userObject(); return postProcess(options); } @@ -328,6 +337,7 @@ private static Options postProcess(Options options) { "at least one of --main-class, --input-classes " + "or --app-class-path should be specified"); } + options.addReflectionLogClasses(); // mkdir for output dir if (!options.outputDir.exists()) { options.outputDir.mkdirs(); @@ -499,6 +509,72 @@ private static String toSerializedFilePath(String file) { .replace('\\', '/'); } + private static class AnalysisOptionsConverter implements CommandLine.ITypeConverter { + @Override + public AnalysisOptions convert(String value) { + ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); + JavaType mapType = mapper.getTypeFactory() + .constructMapType(Map.class, String.class, Object.class); + String optStr = toYAMLString(value); + try { + Map optsMap = optStr.isBlank() + ? Map.of() + // Leverage Jackson to parse YAML string to Map + : mapper.readValue(optStr, mapType); + return new AnalysisOptions(optsMap); + } catch (JsonProcessingException e) { + throw new ConfigException("Invalid analysis options: " + value, e); + } + } + + /** + * Converts option string to a valid YAML string. + * The option string is of format "key1:value1;key2:value2;...". + */ + private static String toYAMLString(String optValue) { + StringJoiner joiner = new StringJoiner("\n"); + for (String keyValue : optValue.split(";")) { + if (!keyValue.isBlank()) { + int i = keyValue.indexOf(':'); // split keyValue + joiner.add(keyValue.substring(0, i) + ": " + + keyValue.substring(i + 1)); + } + } + return joiner.toString(); + } + } + + /** + * Add classes in reflection log to the input classes. + *

+ * TODO: this is still a tentative solution. + */ + private void addReflectionLogClasses() { + List inputClasses = new ArrayList<>(this.inputClasses); + AnalysisOptions analysisOptions = analyses.get(PointerAnalysis.ID); + if (analysisOptions == null || !analysisOptions.has("reflection-log")) { + return; + } + String path = analysisOptions.getString("reflection-log"); + if (path != null) { + LogItem.load(path).forEach(item -> { + // add target class + String target = item.target; + String targetClass; + if (target.startsWith("<")) { + targetClass = StringReps.getClassNameOf(target); + } else { + targetClass = target; + } + if (StringReps.isArrayType(targetClass)) { + targetClass = StringReps.getBaseTypeNameOf(target); + } + inputClasses.add(targetClass); + }); + } + this.inputClasses = List.copyOf(inputClasses); + } + @Override public String toString() { return "Options{" + diff --git a/src/main/java/pascal/taie/config/PlanConfig.java b/src/main/java/pascal/taie/config/PlanConfig.java index 8edd09a62..1c1a4f069 100644 --- a/src/main/java/pascal/taie/config/PlanConfig.java +++ b/src/main/java/pascal/taie/config/PlanConfig.java @@ -24,7 +24,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; @@ -35,9 +34,7 @@ import java.io.File; import java.io.IOException; import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.StringJoiner; /** * Configuration for an analysis to be executed. @@ -108,48 +105,16 @@ public static List readConfigs(File file) { * Reads a list of PlanConfig from options. */ public static List readConfigs(Options options) { - ObjectMapper mapper = new ObjectMapper(new YAMLFactory()); - JavaType mapType = mapper.getTypeFactory() - .constructMapType(Map.class, String.class, Object.class); return options.getAnalyses().entrySet() .stream() .map(entry -> { String id = entry.getKey(); - String optStr = toYAMLString(entry.getValue()); - try { - Map optsMap = optStr.isBlank() - ? Map.of() - // Leverage Jackson to parse YAML string to Map - : mapper.readValue(optStr, mapType); - return new PlanConfig(id, new AnalysisOptions(optsMap)); - } catch (JsonProcessingException e) { - throw new ConfigException("Invalid analysis options: " + - entry.getKey() + ":" + entry.getValue(), e); - } + AnalysisOptions analysisOptions = entry.getValue(); + return new PlanConfig(id, analysisOptions); }) .toList(); } - /** - * Converts option string to a valid YAML string. - * The option string is of format "key1:value1;key2:value2;...". - */ - private static String toYAMLString(String optValue) { - StringJoiner joiner = new StringJoiner("\n"); - for (String keyValue : optValue.split(";")) { - if (!keyValue.isBlank()) { - int i = keyValue.indexOf(':'); // split keyValue - if (i == -1) { - throw new IllegalArgumentException("Invalid argument format '" + keyValue - + "'. Expected format: 'key:value'"); - } - joiner.add(keyValue.substring(0, i) + ": " - + keyValue.substring(i + 1)); - } - } - return joiner.toString(); - } - /** * Writes a list of PlanConfigs to given file. */ diff --git a/src/main/java/pascal/taie/frontend/cache/CachedWorldBuilder.java b/src/main/java/pascal/taie/frontend/cache/CachedWorldBuilder.java index d9db0f0bd..503e0e4fe 100644 --- a/src/main/java/pascal/taie/frontend/cache/CachedWorldBuilder.java +++ b/src/main/java/pascal/taie/frontend/cache/CachedWorldBuilder.java @@ -26,7 +26,6 @@ import org.apache.logging.log4j.Logger; import pascal.taie.World; import pascal.taie.WorldBuilder; -import pascal.taie.config.AnalysisConfig; import pascal.taie.config.Options; import pascal.taie.util.Monitor; @@ -69,12 +68,12 @@ public CachedWorldBuilder(WorldBuilder delegate) { } @Override - public void build(Options options, List analyses) { + public void build(Options options) { File worldCacheFile = getWorldCacheFile(options); if (loadCache(options, worldCacheFile)) { return; } - runWorldBuilder(options, analyses); + runWorldBuilder(options); saveCache(worldCacheFile); } @@ -86,10 +85,8 @@ private boolean loadCache(Options options, File worldCacheFile) { logger.info("Loading the world cache from {}", worldCacheFile); Monitor monitor = new Monitor("Load the world cache"); monitor.start(); - ObjectInputStream ois = null; - try { - ois = new ObjectInputStream( - new BufferedInputStream(new FileInputStream(worldCacheFile))); + try (ObjectInputStream ois = new ObjectInputStream( + new BufferedInputStream(new FileInputStream(worldCacheFile)))) { World world = (World) ois.readObject(); World.set(world); world.setOptions(options); @@ -98,24 +95,17 @@ private boolean loadCache(Options options, File worldCacheFile) { logger.error("Failed to load world cache from {} due to {}", worldCacheFile, e); } finally { - if (ois != null) { - try { - ois.close(); - } catch (Exception e) { - logger.error("Failed to close input stream", e); - } - } monitor.stop(); logger.info(monitor); } return false; } - private void runWorldBuilder(Options options, List analyses) { + private void runWorldBuilder(Options options) { logger.info("Running the WorldBuilder ..."); Monitor monitor = new Monitor("Run the WorldBuilder"); monitor.start(); - delegate.build(options, analyses); + delegate.build(options); monitor.stop(); logger.info(monitor); } diff --git a/src/main/java/pascal/taie/frontend/soot/SootWorldBuilder.java b/src/main/java/pascal/taie/frontend/soot/SootWorldBuilder.java index 5589ce01b..92cbf3b5f 100644 --- a/src/main/java/pascal/taie/frontend/soot/SootWorldBuilder.java +++ b/src/main/java/pascal/taie/frontend/soot/SootWorldBuilder.java @@ -29,13 +29,9 @@ import org.apache.logging.log4j.Logger; import pascal.taie.AbstractWorldBuilder; import pascal.taie.World; -import pascal.taie.analysis.pta.PointerAnalysis; -import pascal.taie.analysis.pta.plugin.reflection.LogItem; -import pascal.taie.config.AnalysisConfig; import pascal.taie.config.Options; import pascal.taie.language.classes.ClassHierarchy; import pascal.taie.language.classes.ClassHierarchyImpl; -import pascal.taie.language.classes.StringReps; import pascal.taie.language.type.TypeSystem; import pascal.taie.language.type.TypeSystemImpl; import soot.G; @@ -67,8 +63,8 @@ public class SootWorldBuilder extends AbstractWorldBuilder { private static final String BASIC_CLASSES = "basic-classes.yml"; @Override - public void build(Options options, List analyses) { - initSoot(options, analyses, this); + public void build(Options options) { + initSoot(options, this); // set arguments and run soot List args = new ArrayList<>(); // set class path @@ -83,8 +79,7 @@ public void build(Options options, List analyses) { runSoot(args.toArray(new String[0])); } - private static void initSoot(Options options, List analyses, - SootWorldBuilder builder) { + private static void initSoot(Options options, SootWorldBuilder builder) { // reset Soot G.reset(); @@ -119,7 +114,6 @@ private static void initSoot(Options options, List analyses, Scene scene = G.v().soot_Scene(); addBasicClasses(scene); - addReflectionLogClasses(analyses, scene); // Configure Soot transformer Transform transform = new Transform( @@ -153,43 +147,6 @@ private static void addBasicClasses(Scene scene) { } } - /** - * Add classes in reflection log to the scene. - * Tai-e's ClassHierarchy depends on Soot's Scene, which does not change - * after hierarchy's construction, thus we need to add the classes - * in the reflection log before starting Soot. - *

- * TODO: this is a tentative solution. We should remove it and use other - * way to load basic classes in the reflection log, so that world builder - * does not depend on analyses to be executed. - * - * @param analyses the analyses to be executed - * @param scene the Soot's scene - */ - private static void addReflectionLogClasses(List analyses, Scene scene) { - analyses.forEach(config -> { - if (config.getId().equals(PointerAnalysis.ID)) { - String path = config.getOptions().getString("reflection-log"); - if (path != null) { - LogItem.load(path).forEach(item -> { - // add target class - String target = item.target; - String targetClass; - if (target.startsWith("<")) { - targetClass = StringReps.getClassNameOf(target); - } else { - targetClass = target; - } - if (StringReps.isArrayType(targetClass)) { - targetClass = StringReps.getBaseTypeNameOf(target); - } - scene.addBasicClass(targetClass); - }); - } - } - }); - } - private void build(Options options, Scene scene) { World.reset(); World world = new World();