Skip to content

Immutability

Kadiray Karakaya edited this page Nov 9, 2022 · 11 revisions

SootUp has been designed with the goal of immutability in mind. This makes sharing objects between several entities easier, because there is no need to worry about unintended changes to other entities.

Missing setters and their substitute

Withers

Due to the immutability goal, many classes do not have setters anymore. For example, a Body does not have a method setStmts(List<Stmt> stmts). Instead, a method called withStmts(List<Stmt> stmts) has been added. This does not modify the original instance, but returns a copy that has different stmts than the original instance. This concept of so-called with-ers can be found all throughout SootUp. A simplified example:

class Body {
  final List<Stmt> stmts;
  final List<Local> locals;

  Body(List<Stmt> stmts, List<Local> locals) {
    this.stmts = stmts;
    this.locals = locals;
  }  

  Body withStmts(List<Stmt> stmts) { return new Body(stmts, this.locals); }
  Body withLocals(List<Local> locals) { return new Body(this.stmts, locals); }
}

Example Use Case: Replacing Statements

To understand this concept in more detail, consider the following example. The application we're analyzing with SootUp contains a class Dog that has two methods wagTail() and bark(). The implementation of bark() prints out "woof". We'd like to localize this dog class and make it print "wuff" for a German audience. This can be achieved by modifying the stmts contained by the Body of the SootMethod belonging to the SootClass. The following code shows how this can be done:

SootClass dogClass = ...;
SootMethod barkMethod = dogClass.getMethod(...);
JAssignStmt wuffAssign = new JAssignStmt(
                           new Local("$s0", DefaultIdentifierFactory.getClassType("java.lang.String")), 
                           new StringConstant("wuff"), 
                           new NoPositionInformation()
                         );
SootClass germanDogClass = dogClass.withReplacedMethod(
                             barkMethod, 
                             barkMethod.withBodyStmts(newStmts -> newStmts.set(0, wuffAssign))
                           );

As illustrated in the below diagram, this creates a new JAssignStmt that assigns "wuff" to the local variable $s0 that contained "woof" before. As entities are immutable, we can't modify the stmts of the initial barkMethod(). Therefore, barkMethod.withBodyStmts(...) returns a new SootMethod in which the statement at position 0 was replaced by the wuffAssign statement we just created. Finally, we need to create a new SootClass for the German dog, because the original SootClass also can't be modified. dogClass.withReplacedMethod(...) does this by replacing the method defined by its first argument with the second argument.

Immutability Diagram

The diagram also illustrates which entities are shared and which have to be newly created. Shared entities are indicated by their green background. In this case, the entire wagTail() method can be shared between the two SootClasses as it was not modified at all. On the other hand, the bark() method has been copied with the exception of the last two statements as we haven't modified those.

Hidden Setters

In some rare cases, it was not possible to remove setters entirely yet. As users are not supposed to use these, they have been made private and exposed through dedicated accessor classes. For example:

class NotQuiteImmutable {
  // ...
  private void setFoo(Foo foo) { /* ... */ }
  public static class $Accessor { 
    /** For internal use only **/
    @Deprecated public static void setFoo(NotQuiteImmutable n, Foo foo) {
      n.setFoo(foo);
    }
  }
}

The deliberately obscure name is supposed to discourage users from using these methods, as they are meant to be internal APIs. While this cannot guarantee that users will not use them regardless, the documentation makes clear that this is unsupported and may be removed in the future. The goal is to get rid of this mechanism as soon as all algorithms that rely on them have been migrated. Additionally, SootUp needs to ensure that a user does not see any changes to objects they consider immutable.

Other kinds of immutability

In some cases, we are forced to return objects conforming to mutable APIs for pragmatic reasons. A lot of APIs return lists and other collections of data, but unfortunately the JDK's Collections API is mutable by default. This means that we cannot hide methods such as List<T>.add(T t) from users; however, we expect users not to call them. To prevent the use of these methods, publicly exposed collections should throw exceptions when mutating methods are called. This can be easily achieved by using the various Collections.unmodifiableXYZ(...) APIs to create a read-only view of a read-write collection or ImmutableUtils.immutableXYZ(...) to create fully immutable collections.