Skip to main content
5 of 10
added 4269 characters in body
Poke
  • 3.2k
  • 14
  • 30

Java

String getParamName(String param) throws Exception {
    StackTraceElement[] strace = new Exception().getStackTrace();
    String methodName = strace[0].getMethodName();
    int lineNum = strace[1].getLineNumber();
    
    String className = strace[1].getClassName().replaceAll(".{5}$", "");
    String classPath = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath() + className + ".class";
    
    BufferedReader javapIn = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("javap -l -c " + classPath).getInputStream()));
    List<String> javapLines = new ArrayList<String>();
    String javapLine = "";
    int byteCodeStart = -1;
    int n = 0;
    Map<Integer, Integer> byteCodePointerToJavaPLine = new HashMap<Integer, Integer>();
    Pattern byteCodeIndexPattern = Pattern.compile("^\\s*(\\d+): ");
    while(true) {
        javapLine = javapIn.readLine();
        if (byteCodeStart > -1 (javapLine == null || "".equals(javapLine))) {
            break;
        }
        Matcher byteCodeIndexMatcher = byteCodeIndexPattern.matcher(javapLine);
        if (byteCodeIndexMatcher.find()) {
            byteCodePointerToJavaPLine.put(Integer.parseInt(byteCodeIndexMatcher.group(1)), n);
        } else if (javapLine.contains("line " + lineNum + ":")) {
            byteCodeStart = Integer.parseInt(javapLine.substring(javapLine.indexOf(": ") + 2));
        }
        javapLines.add(javapLine);
        n++;
    }

    int varLoadIndex = -1;
    int varTableIndex = -1;
    for (int i = byteCodePointerToJavaPLine.get(byteCodeStart) + 1;i < javapLines.size();i++) {
        if (varLoadIndex < 0 && javapLines.get(i).contains("Method " + methodName + ":")) {
            varLoadIndex = i;
            continue;
        }
        
        if (varLoadIndex > -1 && javapLines.get(i).contains("LocalVariableTable:")) {
            varTableIndex = i;
            break;
        }
    }
    
    String loadLine = javapLines.get(varLoadIndex - 1).trim();
    int varNumber;
    try {
        varNumber = Integer.parseInt(loadLine.substring(loadLine.indexOf("aload") + 6).trim());
    } catch (NumberFormatException e) {
        return null;
    }
    int j = varTableIndex + 2;
    while(!"".equals(javapLines.get(j))) {
        Matcher varName = Pattern.compile("\\s*" + varNumber + "\\s*([a-zA-Z_][a-zA-Z0-9_]*)").matcher(javapLines.get(j));  
        if (varName.find()) {
            return varName.group(1);
        }
        j++;
    }
    return null;
}

This currently works with a few gotchas:

  1. If you use an IDE to compile this it might not work unless it is run as Admin (depending on where the temporary class files are saved)
  2. You must compile using javac with the -g flag. This generates all debugging information including local variable names in the compiled class file.
  3. javap must be on your PATH. javap comes with the JDK so if you have that, it shouldn't be an issue. If javap is not in the PATH, you will need to modify where it is being invoked to include the full path to the executable. (I realize this is a crutch to just parsing the bytecode myself but that's going to take lot more time, haha)

This should now work even if the method is called multiple times in the program. Of course it's hacky so I may have also introduced bugs...

Try it online!

Explanation

StackTraceElement[] strace = new Exception().getStackTrace(); String methodName = strace[0].getMethodName(); int lineNum = strace[1].getLineNumber();
String className = strace[1].getClassName().replaceAll(".{5}$", "");
String classPath = Class.forName(className).getProtectionDomain().getCodeSource().getLocation().getPath() + className + ".class";

This first part gets some general information about what class we're in and what the name of the function is. This is accomplished by creating an exception and parsing the first 2 entries of the stack trace. The first entry is the line that the exception is thrown on which we can grab the methodName from and the second entry is where the function was called from.

BufferedReader javapIn = new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("javap -l -c " + classPath).getInputStream()));

In this line we are executing the javap executable that comes with the JDK. This program parses the class file (bytecode) and presents a human-readable result. We'll use this for rudimentary "parsing".

List<String> javapLines = new ArrayList<String>();
String javapLine = "";
int byteCodeStart = -1;
int n = 0;
Map<Integer, Integer> byteCodePointerToJavaPLine = new HashMap<Integer, Integer>();
Pattern byteCodeIndexPattern = Pattern.compile("^\\s*(\\d+): ");
while(true) {
    javapLine = javapIn.readLine();
    if (byteCodeStart > -1 (javapLine == null || "".equals(javapLine))) {
        break;
    }
    Matcher byteCodeIndexMatcher = byteCodeIndexPattern.matcher(javapLine);
    if (byteCodeIndexMatcher.find()) {
        byteCodePointerToJavaPLine.put(Integer.parseInt(byteCodeIndexMatcher.group(1)), n);
    } else if (javapLine.contains("line " + lineNum + ":")) {
        byteCodeStart = Integer.parseInt(javapLine.substring(javapLine.indexOf(": ") + 2));
    }
    javapLines.add(javapLine);
    n++;
}

We're doing a couple different things here. First, we are reading the javap output line by line into a list. Second we are creating a map of bytecode line indexes to javap line indexes. This helps us later to determine which method invocation we want to analyze. Finally we are using the known line number from the stack trace to determine which bytecode line index we want to be looking at.

int varLoadIndex = -1;
int varTableIndex = -1;
for (int i = byteCodePointerToJavaPLine.get(byteCodeStart) + 1;i < javapLines.size();i++) {
    if (varLoadIndex < 0 && javapLines.get(i).contains("Method " + methodName + ":")) {
        varLoadIndex = i;
        continue;
    }

    if (varLoadIndex > -1 && javapLines.get(i).contains("LocalVariableTable:")) {
        varTableIndex = i;
        break;
    }
}

Here we are iterating over the javap lines one more time in order to find the spot where our method is being invoked and where the Local Variable Table starts. We need the line where the method is invoked because the line before it contains the call to load the variable and identifies which variable (by index) to load. The Local Variable Table helps us actually look up the name of the variable based on the index we grabbed.

String loadLine = javapLines.get(varLoadIndex - 1).trim();
int varNumber;
try {
    varNumber = Integer.parseInt(loadLine.substring(loadLine.indexOf("aload") + 6).trim());
} catch (NumberFormatException e) {
    return null;
}

This part is actually parsing the load call to get the variable index. This can throw an exception if the function isn't actually called with a variable so we can return null here.

int j = varTableIndex + 2;
while(!"".equals(javapLines.get(j))) {
    Matcher varName = Pattern.compile("\\s*" + varNumber + "\\s*([a-zA-Z_][a-zA-Z0-9_]*)").matcher(javapLines.get(j));  
    if (varName.find()) {
        return varName.group(1);
    }
    j++;
}
return null;

Finally we parse out the name of the variable from the line in the Local Variable Table. Return null if it isn't found although I've seen no reason why this should happen.

Poke
  • 3.2k
  • 14
  • 30