I'm trying to parse the java
classes in order to populate my objects with data - using the JDT AST. Most of the time this works just fine. However, there seems to be an issue with the java.util.Locale
class.
While other classes get parsed as expected (to the best of my knowledge), the java.util.Locale
fails when it comes to the methods directly after the statically nested class LanguageRange
.
Now, I borrowed some code from this question and modified it to suit my needs to quickly setup a test environment.
Example
public static void parse(String code) {
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setSource(code.toCharArray());
parser.setKind(ASTParser.K_COMPILATION_UNIT);
final CompilationUnit cu = (CompilationUnit) parser.createAST(null);
cu.accept(new ASTVisitor() {
public boolean visit(MethodDeclaration method) {
if(method.getName().toString().equals("filter")){
debug("method", method.getName().getFullyQualifiedName());
if(method.getParent().getNodeType() == ASTNode.TYPE_DECLARATION){
TypeDeclaration parentClass = TypeDeclaration.class.cast(method.getParent());
debug("Parent", parentClass.getName().toString());
}
}
return false;
}
});
}
public static void debug(String ref, String message) {
System.out.println(ref + ": " + message);
}
With this code, the exact same thing happens, so I'm not quite sure whether I'm missing something or I found a bug.
As for what is happening, the filter
method gets detected, as expected. However when accessing the parent, it becomes clear that the wrong parent was computed. This is, because Locale
should be the parent's name, but it is LanguageRange
.
Output
method: filter
Parent: LanguageRange
Note that it is assumed that the java.util.Locale
class was used as input.
Did anyone experience this issue before? How would I go around it, in order to safely determine the parent of the method?
UPDATE
I tested some other classes as well and it seems they work just fine. Which makes it even more confusing.
Below is a sample taken from Programm Creek, which I, again, altered to suit my needs.
Sample
package TEST;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
import org.eclipse.jdt.core.dom.VariableDeclarationFragment;
import org.eclipse.jdt.core.dom.VariableDeclarationStatement;
import app.configuration.Configuration;
public class ASTTester {
public static void main(String[] args) {
String srcPath = Configuration.app.getPaths().get("api") + "src"; // Absolute path to src folder
String unitName = "Locale.java"; // Name of the file to parse
String path = srcPath + "\\java\\util\\" + unitName; // Absoulte path to the file to parse
File file = new File(path);
String str = "";
try {
str = Files.lines(Paths.get(file.getAbsolutePath())).reduce((l1, l2) -> l1 + System.lineSeparator() + l2).orElse("");
} catch (IOException e) {
e.printStackTrace();
}
ASTParser parser = ASTParser.newParser(AST.JLS8);
parser.setResolveBindings(true);
parser.setKind(ASTParser.K_COMPILATION_UNIT);
parser.setBindingsRecovery(true);
Map options = JavaCore.getOptions();
parser.setCompilerOptions(options);
parser.setUnitName(unitName);
String[] sources = { srcPath };
String[] classpath = {"C:\\Program Files\\Java\\jre1.8.0_121\\lib\\rt.jar"}; // May need some altering
parser.setEnvironment(classpath, sources, new String[] { "UTF-8"}, true);
parser.setSource(str.toCharArray());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
if (cu.getAST().hasBindingsRecovery()) {
System.out.println("Binding activated.");
}
TypeFinderVisitor v = new TypeFinderVisitor();
cu.accept(v);
}
}
class TypeFinderVisitor extends ASTVisitor{
public boolean visit(VariableDeclarationStatement node){
for (Iterator<?> iter = node.fragments().iterator(); iter.hasNext();) {
System.out.println("------------------");
VariableDeclarationFragment fragment = (VariableDeclarationFragment) iter.next();
IVariableBinding binding = fragment.resolveBinding();
System.out.println("binding variable declaration: " +binding.getVariableDeclaration());
System.out.println("binding: " +binding);
}
return true;
}
public boolean visit(TypeDeclaration clazz){
ITypeBinding binding = clazz.resolveBinding();
if(binding != null){
System.out.println("################ BINDING ##############");
System.out.println(binding);
System.out.println("##############################");
for (IMethodBinding method : binding.getDeclaredMethods()) {
System.out.println(clazz.getName().toString() + ": " + method.getName().toString());
}
}
return true;
}
}
The output has a different format, but the result is the same.
Result
// Omitted...
LanguageRange: LanguageRange
LanguageRange: LanguageRange
LanguageRange: equals
LanguageRange: filter
LanguageRange: filter
LanguageRange: filterTags
LanguageRange: filterTags
LanguageRange: getRange
LanguageRange: getWeight
LanguageRange: hashCode
// Omitted...
However, when testing a smaller version of the exact same case, the result is correct.
Example Class
package TEST;
public class Test {
public void methodBefore(){
}
public static class Inner{
public static void foo(){
}
}
public static class Inner2{
public static void foo2(){
}
}
public void methodAfter(){
}
}
Output
Test: Test
Test: methodAfter
Test: methodBefore
Since the example class Test
is working, I assume that I'm missing something. But what?
Note that I use the AST parser standalone (i.e. I merely included the necessary libraries - which means I don't have access to classes like IProject
and such).
Finally, I found the solution to my problem! I researched some more and found this answer. While it does not seem to solve the OP's problem, it sure gave me an important hint.
I updated one of my classes accordingly (see below).
Example
package TEST;
import java.util.Map;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.TypeDeclaration;
public class ASTBug {
public static void parse(String code) {
ASTParser parser = ASTParser.newParser(AST.JLS8);
Map<String, String> options = JavaCore.getOptions();
options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_8);
parser.setCompilerOptions(options);
// Create a compilation unit
parser.setSource(code.toCharArray());
CompilationUnit cu = (CompilationUnit) parser.createAST(null);
cu.accept(new ASTVisitor() {
public boolean visit(TypeDeclaration clazz) {
System.out.println("########### START ############");
for (MethodDeclaration method : clazz.getMethods()) {
System.out.println(clazz.getName().toString() + ": " + method.getName().toString());
}
System.out.println("########### END ############");
return true;
}
});
}
}
Note that now the options are set properly: options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_8);
.
It makes sense, though. The default version is JavaCore.Version_1_3
(as stated in the before-mentioned answer) and since java.util.Locale
contains enums, which are supported starting with JavaCore.VERSION_1_5
(again, stated in the before-mentioned answer) the parser gets screwed up. Or at least that's what I concluded from my inspections.
However, at first it still did not work. The solution was quickly found, but I'm not too certain about why this is necessary (see below). I used only 1 ASTParser
instance, instead of 1 per file, which seemed to somehow screw things up.
In any case, it was not a bug, I just missed to properly set the options of the parser.
Last but not least, have some class, which should work for Java 8
and below.
ASTCreator
package ast;
import java.util.Arrays;
import java.util.Map;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTParser;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
public class ASTCreator{
/**
* The AST parser for parsing the Java files.
*/
private ASTParser parser;
/**
* The compilation unit create through the AST parser.
*/
private CompilationUnit compilationUnit;
/**
* Creates the parser with the specified setting.
*/
private void create(){
this.parser = ASTParser.newParser(AST.JLS8);
Map<String, String> options = JavaCore.getOptions();
options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_8);
this.parser.setCompilerOptions(options);
}
/**
* Accepts the given visitors, which specify what to do with the parsed Java file.
* @param fileContent the content of the file to parse
* @param visitors the visitors to accept
*/
public void accept(String fileContent, ASTVisitor... visitors){
this.create();
this.parser.setSource(fileContent.toCharArray());
this.compilationUnit = (CompilationUnit) parser.createAST(null);
Arrays.stream(visitors).forEach(this.compilationUnit::accept);
}
}
Create a new instance of this class (e.g. astCreator
) and use it like this:
astCreator.accept(fileReader.read(file), myVisitor);
. Obviousely, fileReader
and myVisitor
need to be replaced with your own code.