Skip to content

From Prototype to a shareable and usable analysis Tool

The problem

Most SootUp analyses start the same way: a hardcoded path in a main method, no way to change the target without recompiling, a README that says "edit line 3". That is fine for a prototype, but turns into friction the moment someone else wants to use your work.

This proposes is CLI option parsing layer. Rather than designing one from scratch. This page walks through SootUpConfiguration you can copy it directly.

The flag names deliberately mirror the java executable, so users who already know and can copy & paste their execution options e.g. from their IDE.

1
java -cp myapp.jar:libs/* com.example.Main

can immediately guess how to invoke a SootUp tool built on this pattern.


Dependency

1
2
3
4
5
<dependency>
    <groupId>commons-cli</groupId>
    <artifactId>commons-cli</artifactId>
    <version>1.11.0</version>
</dependency>
1
implementation("commons-cli:commons-cli:1.11.0")

Defining the flags

The options below cover the most common scenarios: classpath, module path, an executable JAR, and a module-style entry point. Every flag that java accepts for specifying where to find code has a direct equivalent here:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    Options options = new Options();
    // Long-only options: Option.builder() without opt() gives a long-only flag (no -x short form).
    options.addOption(
        Option.builder()
            .longOpt("jar")
            .hasArg()
            .argName("file")
            .desc("Executable JAR; Main-Class is read from its manifest.")
            .build());
    options.addOption(
        Option.builder()
            .longOpt("class-path")
            .hasArg()
            .argName("path")
            .desc(
                "Application classpath (directories or JARs, separated by '"
                    + java.io.File.pathSeparator
                    + "').")
            .build());
    options.addOption(
        Option.builder()
            .longOpt("classpath")
            .hasArg()
            .argName("path")
            .desc("Application classpath (alias for --class-path).")
            .build());
    options.addOption(
        Option.builder()
            .longOpt("cp")
            .hasArg()
            .argName("path")
            .desc("Application classpath (short alias).")
            .build());
    // Options with both a short and a long name (mirrors java/javac flags).
    options.addOption(
        Option.builder("m")
            .longOpt("module")
            .hasArg()
            .argName("module")
            .desc("Module entry point, e.g. com.example/com.example.Main.")
            .build());
    options.addOption(
        Option.builder("p")
            .longOpt("module-path")
            .hasArg()
            .argName("path")
            .desc("Module path.")
            .build());

Why mirror the java flags?
Muscle memory. Anyone who has ever run javac, java, or jar already knows that -cp / --classpath / --class-path all mean the same thing. Inventing new names (--input, --target, --binary) adds a cognitive hurdle that pays nothing back.


Constructing the View

Each flag maps to exactly one SootUp API call. Classpath entries become JavaClassPathAnalysisInputLocations; module paths become JavaModulePathAnalysisInputLocations. The JRE is added automatically as SourceType.Library so that java.lang.* and friends always resolve:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
    // Collect all --cp / --classpath / --class-path values (all three are accepted, just like
    // java).
    for (String opt : new String[] {"cp", "classpath", "class-path"}) {
      if (cmd.hasOption(opt)) {
        locations.add(
            new JavaClassPathAnalysisInputLocation(
                cmd.getOptionValue(opt),
                SourceType.Application,
                BytecodeBodyInterceptors.Default.getBodyInterceptors()));
      }
    }

    if (locations.isEmpty()) {
      // Fall back to the current working directory when nothing is specified.
      locations.add(
          new JavaClassPathAnalysisInputLocation(
              System.getProperty("user.dir"),
              SourceType.Application,
              BytecodeBodyInterceptors.Default.getBodyInterceptors()));
    }

The entry point is resolved from three sources in order of precedence: --jar (reads Main-Class from the manifest), --module, or the first positional argument (the main class name). The rest of the positional arguments are forwarded as arguments to simulate passing String[] to main:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
    List<String> positional = cmd.getArgList();

    if (cmd.hasOption("jar")) {
      String jarFile = cmd.getOptionValue("jar");
      locations.add(
          new JavaClassPathAnalysisInputLocation(
              jarFile,
              SourceType.Application,
              BytecodeBodyInterceptors.Default.getBodyInterceptors()));

      // Read Main-Class from the JAR manifest — no need to specify it separately.
      try (JarInputStream jis = new JarInputStream(new FileInputStream(jarFile))) {
        Manifest manifest = jis.getManifest();
        Attributes attrs = manifest.getMainAttributes();
        String mainClass = attrs.getValue("Main-Class");
        this.entrypoint =
            new MethodSignature(
                JavaIdentifierFactory.getInstance().getClassType(mainClass), MAIN_SIGNATURE);
      }
    } else if (cmd.hasOption("module")) {
      this.entrypoint =
          new MethodSignature(
              JavaModuleIdentifierFactory.getInstance().getClassType(cmd.getOptionValue("module")),
              MAIN_SIGNATURE);
    } else {
      if (positional.isEmpty()) {
        throw new IllegalArgumentException("No main class specified and no --jar/--module given.");
      }
      // First positional argument is the main class; the rest are forwarded to main(String[]).
      String mainClass = positional.remove(0);
      this.entrypoint =
          new MethodSignature(
              JavaIdentifierFactory.getInstance().getClassType(mainClass), MAIN_SIGNATURE);
    }

Using it in your tool

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) throws Exception {
    SootUpConfiguration config = new SootUpConfiguration(args);

    JavaView view          = config.getView();
    MethodSignature entry  = config.getEntrypoint();
    List<String> mainArgs  = config.getArguments();   // remaining positional args

    // Everything from here is identical to any other SootUp analysis.
    view.getClasses().forEach(c -> System.out.println(c.getName()));
}

Compared to a hardcoded prototype, only the top three lines change. All analysis code below them stays the same.


Supported invocation patterns

What you want Command-line
Analyse a directory of .class files mytool --cp target/classes com.example.Main
Analyse an executable JAR mytool --jar myapp.jar
Classpath with multiple entries mytool --cp "app.jar:libs/*" com.example.Main
Module path mytool -p mods -m com.example/com.example.Main
All three aliases are equivalent --cp = --classpath = --class-path