Skip to content

[Feature]: Prune unnecessary <clinit> calls #1421

@jecisc

Description

@jecisc

What happened?

Hi,

I tried to build the call graph of this project:

Operation.java

/**
 * A simple enum that represents arithmetic operations.
 * Each constant overrides {@link #apply(int,int)}.
 * The enum also provides a static helper {@code execute} that
 * demonstrates invoking the enum methods from client code.
 */
public enum Operation {

    /** Addition */
    ADD {
        @Override
        public int apply(int a, int b) {
            return a + b;
        }
    },

    /** Subtraction */
    SUBTRACT {
        @Override
        public int apply(int a, int b) {
            return a - b;
        }
    },

    /** Multiplication */
    MULTIPLY {
        @Override
        public int apply(int a, int b) {
            return a * b;
        }
    },

    /** Division – integer division, guards against divide‑by‑zero */
    DIVIDE {
        @Override
        public int apply(int a, int b) {
            if (b == 0) {
                throw new ArithmeticException("Division by zero");
            }
            return a / b;
        }
    };

    /** Abstract method each constant must implement */
    public abstract int apply(int a, int b);

    /**
     * Utility that parses a textual operator and forwards the call
     * to the corresponding enum constant.
     *
     * @param opSymbol one of "+", "-", "*", "/"
     * @param left     left operand
     * @param right    right operand
     * @return result of the operation
     */
    public static int execute(String opSymbol, int left, int right) {
        switch (opSymbol) {
            case "+":
                return ADD.apply(left, right);
            case "-":
                return SUBTRACT.apply(left, right);
            case "*":
                return MULTIPLY.apply(left, right);
            case "/":
                return DIVIDE.apply(left, right);
            default:
                throw new IllegalArgumentException("Unsupported operator: " + opSymbol);
        }
    }
}

Main.java

/**
 * Simple driver that exercises the {@link Operation} enum.
 * Run it with:  java com.example.Main
 */
public class Main {

    public static void main(String[] args) {
        // Hard‑coded sample data
        int a = 12;
        int b = 4;

        // Direct enum method calls
        System.out.println("Direct enum calls:");
        System.out.printf("%d + %d = %d%n", a, b, Operation.ADD.apply(a, b));
        System.out.printf("%d - %d = %d%n", a, b, Operation.SUBTRACT.apply(a, b));
        System.out.printf("%d * %d = %d%n", a, b, Operation.MULTIPLY.apply(a, b));
        System.out.printf("%d / %d = %d%n", a, b, Operation.DIVIDE.apply(a, b));

        // Calls via the static helper
        System.out.println("\nCalls via Operation.execute(...):");
        System.out.printf("%d + %d = %d%n", a, b, Operation.execute("+", a, b));
        System.out.printf("%d - %d = %d%n", a, b, Operation.execute("-", a, b));
        System.out.printf("%d * %d = %d%n", a, b, Operation.execute("*", a, b));
        System.out.printf("%d / %d = %d%n", a, b, Operation.execute("/", a, b));

        // Demonstrate exception handling (optional)
        try {
            Operation.execute("/", a, 0);
        } catch (ArithmeticException ex) {
            System.out.println("\nCaught expected exception: " + ex.getMessage());
        }
    }
}

I built it this way:

package com.example;


import sootup.callgraph.CallGraph;
import sootup.callgraph.CallGraphAlgorithm;
import sootup.callgraph.ClassHierarchyAnalysisAlgorithm;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.signatures.MethodSignature;
import sootup.core.types.VoidType;
import sootup.java.bytecode.frontend.inputlocation.DefaultRuntimeAnalysisInputLocation;
import sootup.java.bytecode.frontend.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.types.JavaClassType;
import sootup.java.core.views.JavaView;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CallGraphDemo {
    public static void main(String[] args) {
        String cpString = "/Users/cyril/Library/Preferences/pharo/GitRepositories/jecisc/Famix-CallGraphs/resources/sources/example5/out/production/example5/";

        List<AnalysisInputLocation> inputLocations = new ArrayList<>();
        inputLocations.add(new JavaClassPathAnalysisInputLocation(cpString));
        inputLocations.add(new DefaultRuntimeAnalysisInputLocation());

        JavaView view = new JavaView(inputLocations);

        JavaClassType classTypeA = view.getIdentifierFactory().getClassType("Main");
        System.out.println(classTypeA);

        MethodSignature entryMethodSignature =
                view.getIdentifierFactory()
                        .getMethodSignature(
                                classTypeA,
                                "main",
                                "void",
                                Collections.singletonList("java.lang.String[]")
                        );

        System.out.println(entryMethodSignature);

        CallGraphAlgorithm cha = new ClassHierarchyAnalysisAlgorithm(view);

        CallGraph cg = cha.initialize(Collections.singletonList(entryMethodSignature));
        System.out.println(cg);
        //cg.callsFrom(entryMethodSignature).forEach(System.out::println);
    }
}

In the result I got this:

<Main: void main(java.lang.String[])>:
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: void <clinit>()>
	to <Operation: int execute(java.lang.String,int,int)>
	to <Operation: int execute(java.lang.String,int,int)>
	to <Operation: int execute(java.lang.String,int,int)>
	to <Operation: int execute(java.lang.String,int,int)>
	to <Operation: int execute(java.lang.String,int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: java.io.PrintStream printf(java.lang.String,java.lang.Object[])>
	to <java.io.PrintStream: void println(java.lang.String)>
	to <java.io.PrintStream: void println(java.lang.String)>
	to <java.io.PrintStream: void println(java.lang.String)>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: void <clinit>()>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.Integer: java.lang.Integer valueOf(int)>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.System: void <clinit>()>
	to <java.lang.Throwable: java.lang.String getMessage()>

First I am surprised to find so many static initialization. I would have expected only one time the line to <Operation: void <clinit>()>.

But my biggest surpprize is to find 16 times a call to apply.

	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$1: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$2: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$3: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>
	to <Operation$4: int apply(int,int)>

I have 4 calls to apply. Is it because with the CHA we consider that one call could lead to any of the possible enum values?

I'm tagging this as a bug, but it might be right and me wrong. I'd just like to understand the logic :)

Version used: <sootup.version>develop-19a4ac63ff-1</sootup.version>

Version

Latest develop branch

Relevant log output

Metadata

Metadata

Labels

call graphThis issues is related to the call graph constructionimprovementnew feature, improve in readability, structure or performance

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions