How to change PSIClass (java files) ?
Hello,
I am trying to parse some java code and modify some of it's contents (method names, variable types) and currently I can iterate through all .java files on the project folder but now I can't change the text content of the PSI Class referring to the .java file since there is no setText() method in the PsiClass class. Here is my current code:
// Translation will be made here
Processor<PsiClass> processor = new Processor<PsiClass>() {
@Override
public boolean process(PsiClass psiClass) {
// Do work here
System.out.println(psiClass.getText());
return true;
}
};
// Get all JavaClasses on project folder
AllClassesGetter.processJavaClasses(
new PlainPrefixMatcher(""),
project,
GlobalSearchScope.projectScope(project),
processor
);
Please sign in to leave a comment.
What kind of changes are you trying to achieve? Please note that AllClassesGetter is likely not the best way to do whatever you want (see http://stackoverflow.com/a/39204590/197205 for a better way)
I am trying to modify the text in the PsiClass but I think that may not be possible. I saw the link you sent me but I was a little confused, can you give me a really simple example on how to use your way to address this issue?
Thanks in advance!
I'm afraid I can't provide any examples until I understand the task you're trying to achieve. You likely don't want to perform a generic change (e.g. replace "public class String { ...}" with a simple "foo"), do you?
The goal I am trying to achieve is to parse the whole java file (represented by the PsiClass) and make some modifications, I will show a brief example:
Let's say this is the .java original file:
I would like to parse the file's text and then make a change like changing the body of the constructor (I will use a specific Java parser API for that) so it would save the file with this content:
Can you understand the task I want to achieve? I can give you more details if needed!
Thanks, it's clearer now. What if a java file has several classes, some of them local or anonymous?
(BTW a java file is not represented by PsiClass, it's represented by PsiJavaFile).
The API deals well with several classes on the same file but it fails to parse anonymous classes or methods so on a first version I will not deal with that. So do I need to change the way I iterate through all the project files so I can use PsiJavaFile ? And if I do so, can I modify the file's text with ease?
In this case, it seems that AllClassesSearch in a project scope is the shortest way to find all classes. But beware, it will also return non-Java classes if there are any in your project.
As for modifications, I assume you've read this: http://www.jetbrains.org/intellij/sdk/docs/basics/architectural_overview.html
You can do the change via PSI or via document. If there can be several changes in one file, modifying PSI might be more convenient as you wouldn't need to track the offset shifts. But you'll have to express the change in PSI terms (e.g. PsiClass#getConstructors, PsiMethod#getBody, PsiCodeBlock#add, JavaPsiFacade.getElementFactory().createStatementFromText).
If you do the change via document, you only have to get offsets from class constructors (getTextRange().getStartOffset())
I will try to modify via PSI but the way you sent with PsiCodeBlock#add wouldn't only add code? I will need to replace some names in the file's code. Also, what does JavaPsiFacade.getElementFactory().createStatementFromText() do or how would I use it ?
Your example specified adding a statement to the constructor code block. If you want something else, why didn't you say so from the beginning? :)
What are the criteria you'd use to find those names to replace?
createStatementFromText takes a text and returns a lightweight PsiStatement object that you can later add to a code block or another statement via PsiElement#add/replace and other similar methods.
You can also replace the whole constructor using replace(factory.createMethodFromText(...)), or its body (createCodeBlockFromText).
I think I said it on the first post by saying "I am trying to parse some java code and modify some of it's contents (method names, variable types)" but sorry for the mislead!
The criteria would be string comparison, I have some imports, some variable types and some method calls that I would look for and change directly or if they are not on the file, I would simply add the new ones.
So what you are suggesting is if I change a method to create a new one with the modifications and then use replace(createMethodFromText(...)) to change a whole method or replace(createCodeBlockFromText) to only change a method's body?
If it's convenient for you to change the method as a whole, then I'd suggest replace.
It's also possible to do finer-grained changes in the references and method calls, but this will require your plugin to traverse the method body PSI, find the elements it concerns and change them (probably also via replace; PsiElementFactory can create all kinds of replacement stuff from text).
If I want to replace only one line on a method's body, how would you advise to do that?
If it's a statement, then you can find this statement (by offset or by enumerating all statements and checking if it's the one you need) and then replace(anotherStatementCreatedWithPsiElementFactory).
How can I enumerate all statements on a certain PsiClass or PsiMethod?
You can enumerate all elements (and check for instanceof PsiStatement) with a PsiRecursiveElementWalkingVisitor: call rootPsiElement.accept(visitor)
Can you please give me a simple example of that?
Thank you for the example, what is the role of the constructor object? What type is it ? Also, can't I enumerate all elements on a PsiClass object?
"constructor" is a PsiMethod returned from PsiClass#getConstructors which I mentioned earlier. Please try calling the methods I mention, and reading the documentation around them, this might help.
I don't know what you mean by "all elements on a PsiClass object", but in the last two my comments, I've been describing how to enumerate all children (direct and indirect) PsiElement-s on any PsiElement (including PsiClass).
But doesn't the method getConstructors() only return the constructors of the java classes? I made a simple test with that method with a java file containing 2 methods (one is a constructor, another isn't) but it only returned one PsiMethod instead of two.
Yes, it does. Your example was about constructors, that's why I mentioned it. There's also PsiClass#getMethods, PsiClass#getFields etc. Please check all PsiClass methods, there's a lot of stuff there. Needless to say, PsiRecursiveElementWalkingVisitor works on all those.
So I will need to iterate through all methods and all fields separately right? When I asked about using a PsiClass object was to see if I could enumerate all PsiElements using your example and then verifying if the Element was a PsiField, a PsiConstructor or a PsiMethod, but I think with the information you gave me I can't do what I want this way. Nonetheless, I think I can now do all the steps I wanted my plugin to do!
Thank you for all your help and time!
Yes, you certainly can iterate over all children of PsiClass with PsiRecursiveElementWalkingVisitor in one go, as I've written already.
Is there a way to convert PsiClass to PsiJavaFile? I ask this because I need to use getPackageName() and getImportList() from PsiJavaFile to modify the packageName and a specific import.
(PsiJavaFile) psiClass.getContainingFile()
I am trying to add a package statement to a PsiJavaFile using the code below but I get an error, do you have any idea on what is causing this?
Did you get an error message together with the exception? It should be "Must not change PSI outside command or undo-transparent action. See com.intellij.openapi.command.WriteCommandAction or com.intellij.openapi.command.CommandProcessor", which suggests which classes to use to fix it (unfortunately, not documented; we'll fix that).
The easiest way to get rid of the exception is to wrap the whole modification into WriteCommandAction.runWriteCommandAction(project, modificationRunnable).
Yes, I got the error message. Can you elaborate on how can I fix the exception? What should the parameter modificatioRunnable be?
A runnable performing your PSI modification, e.g. the whole body of changePackage method
So I need to create a Runnable Object and than run WriteCommandAction.runWriteCommandAction(project, modificationRunnable) ?