JavaFindUsagesHelper does not find references properly directly after changing codes.

Answered

Hello!

I'm currently working on the plugin that finds direct access of field and converts them to getter or setter with automatically generating getter or setter.

For example, when the plugin finds variable.field then it converts variable.getField() and create getter getField() in the containing class of that variable.

To accomplish this, I wrote those codes.

First, collect all child source files of current location (editor popup menu)

val psiFiles = ApplicationManager.getApplication().runReadAction<List<PsiFile>> {
val srcFiles = FilenameIndex.getAllFilesByExt(e.project!!,"java").filter { file -> file.path.startsWith(currentFile.path) }
val psiFiles = srcFiles.mapNotNull {
manager.findFile(it)
}
psiFile
}

Next, search all variables in classes I collected above.

ProgressManager.getInstance().runInReadActionWithWriteActionPriority({
try{
for (psiFile in psiFiles) {
val searcher = VariableSearcher(psiFile)

searcher.startWalking()

progressWindow.text = "Processing ${psiFile.name}"
searcherList.add(searcher)
}
}catch (e:ProcessCanceledException){
return@runInReadActionWithWriteActionPriority
}
}, progressWindow)

 

VariableSearcher works by looking up all variables using visitVariable method of JavaRecursiveElementWalkingVisitor and store them to hash map. It stores variables per class using (data class JavaClassInfo(val psiClass: PsiClass, val variables: MutableSet<JavaVariableInfo>))

---------

Then start processing variable references per class.

I wrote methods : processClass(filter static final field and execute processVariable method) - processVariable(call processReadReference/processWriteReference to find references of variable) - processReadAccess/processWriteAccess(it calls JavaFindUsagesHelper.processElement to find read / write access reference of variable) - processReadReference/processWriteReference(sub routine of processRead/WriteAccess method, it is where actually process variable references)

 

Method: processClass

private fun processClass(classInfo: JavaClassInfo) {
for (variableInfo in classInfo.variables) {
val modifierList = ApplicationManager.getApplication().runReadAction<PsiModifierList> {
variableInfo.pointer.element!!.children.filterIsInstance<PsiModifierList>().first()
}

val skip = invokeReadAction {
modifierList.hasModifierProperty(PsiModifier.STATIC) && modifierList.hasModifierProperty(PsiModifier.FINAL) || isUpperUnderscore(
variableInfo.pointer.element!!.name!!
)//and check whether this variable does not need getter or setter(static final field)
}
if (skip)
continue

val indicator = ProgressManager.getGlobalProgressIndicator()!!
ApplicationManager.getApplication()
.runReadAction { indicator.text = "Processing ${variableInfo.pointer.element?.text}" }

processVariable(variableInfo, classInfo.psiClass, modifierList)
}
}

 

---

Method: processVariable

    private fun processVariable(variableInfo: JavaVariableInfo, contextClass: PsiClass, modifierList: PsiModifierList) {
val element = invokeReadAction<PsiElement> { variableInfo.pointer.element!! }
ProgressManager.getGlobalProgressIndicator()?.text =
"Processing ${invokeReadAction<String> { element.text }}"

val variableField = invokeReadAction { variableInfo.pointer.element!! }

if (variableField !is PsiField)
return

val fieldMember =
invokeReadAction { PsiFieldMember(variableField) }



//It uses GenerationInfo to generate getter or setter.

//Also, GenerateMembersUtil to create getter or setter name of variable.
val generatedGetter = invokeWriteAction<GenerationInfo?> { fieldMember.generateGetter() }
val generatedSetter =
invokeWriteAction<GenerationInfo?> { fieldMember.generateSetter() }



//I found that fieldMember.generateSetter() == null means that there is already the same method.

//In this case, just create name of getter / setter.

val getterPrototype: String =
invokeReadAction { if (generatedGetter == null) GenerateMembersUtil.suggestGetterName(variableField) else generatedGetter.psiMember!!.name!! }
val setterPrototype: String =
invokeReadAction { if (generatedSetter == null) GenerateMembersUtil.suggestSetterName(variableField) else generatedSetter.psiMember!!.name!! }

//It calls processReadAccess

val needGetter = processReadAccess(element) {
processReadReference(
it.element!!,
variableInfo.pointer.element!!.nameIdentifier!!,
getterPrototype
)
}


val needSetter = processWriteAccess(element) {
processWriteReference(
it.element!!,
setterPrototype,
)
}
val psiFile = invokeReadAction<PsiFile> { contextClass.containingFile }
if (psiFile !is PsiJavaFile)
return


if (needGetter && generatedGetter != null) {
invokeWriteAction {
generatedGetter.insert(
variableField.containingClass!!,
variableField.containingClass!!.lastChild,
true
)
}
}
if (needSetter && generatedSetter != null) {
invokeWriteAction {
generatedSetter.insert(
variableField.containingClass!!,
variableField.containingClass!!.lastChild,
true
)
}
}

invokeWriteAction {
modifierList.setModifierProperty("private", true)

}
}

--

private fun processReadAccess(element: PsiElement, compute: (usageInfo: UsageInfo) -> Unit): Boolean {
return JavaFindUsagesHelper.processElementUsages(element, JavaVariableFindUsagesOptions(project).apply {
isReadAccess = true
isWriteAccess = false
searchScope = GlobalSearchScope.allScope(project)
}) {
compute(it)
true
}
}

private fun processWriteAccess(element: PsiElement, compute: (usageInfo: UsageInfo) -> Unit): Boolean {
return JavaFindUsagesHelper.processElementUsages(element, JavaVariableFindUsagesOptions(project).apply {
isReadAccess = false
isWriteAccess = true
searchScope = GlobalSearchScope.allScope(project)
}) {
compute(it)
true
}
}

---

 

    private fun processReadReference(
element: PsiElement,
identifier: PsiIdentifier,
getterPreGenerated: String,
) {

//Find bar at foo.bar
val targetIdentifier = element.children.find { e -> e.text.equals(identifier.text) }!!
val isImportStatement = element is PsiImportStaticReferenceElement
val getter =
factory.createExpressionFromText(
generateGetter(getterPreGenerated, isImportStatement),
null
)

//change foo.bar to foo.getBar()
invokeWriteAction {
targetIdentifier.replace(getter)
}
}

private fun processWriteReference(
element: PsiElement,
setterPreGenerated: String
) {
val parent = invokeReadAction<PsiElement> { element.parent }
if (parent !is PsiAssignmentExpression)
return

val targetIdentifier =
parent.lExpression.children.filterIsInstance<PsiIdentifier>().last()

val isImportStatement = element is PsiImportStaticReferenceElement

val setter = factory.createExpressionFromText(
setterPreGenerated.plus(if (isImportStatement) "" else "(${parent.rExpression?.text})"), null
)

invokeWriteAction {

//In shape, a = b

//remove = and b

//then add setA(b)

parent.operationSign.delete()
parent.rExpression?.delete()
targetIdentifier.replace(setter)
}
}

----

These codes works without any errors. But I found that some special situations.

For example, when i have two variable reference of firstVariable and bar(assume that foo and bar are both variable) in code 'foo.bar',

my plugin first finds 'foo' reference and change it to getFoo().bar and next, my plugin tries to find references of bar, but it cannot find getFoo().bar.

I guess it is because after changing foo to getFoo(), this change has not been applied to ide, so my intellij ide does not find it.

But I can't find the way of fixing this. Help me!

 

 

0
5 comments

Hi Ryu,

If I understand correctly, your pointer to the "foo" PSI element doesn't survive tree reparsing. In turn, the "bar" also is invalid - if it is correct, I suggest trying to change the "variables" set to list and sort them by the offset in file descending (the higher offsets first) before you start modifications. It should guarantee that "bar" will be changed before "foo", which should allow "foo" to survive reparsing.

0

Thank you for replying!

But what if there is also reference 'bar.foo' or 'one.bar.two'? In this case, how do I sort the list?

0

You are right, it won't work in this case.

I don't see any other option than researching references after reparsing the tree, so something like proceeding elements one by one, e.g., find one field, convert it to getter and replace occurrences, proceed with the next, and so on. Smart pointers don't seem to be enough in this case, as pointed elements will not survive when public field access is replaced with a method call.

0

I researched some ways to solve the problem.

1. Search variables in the same manner.

2. Search references and *NOT* process reference(replace element), just record where the reference is(maybe offset?).

2-1. If the location of the reference is already recorded, just add information(location) of identifier to the record.

3. After step 2 finished, look up records and process reference in sequence (right to left)

I think this is the best way to solve problem.

Would you give me an advice?

0

I think i solved the problem using mechanism above.

+) And I newly started to using 

QuickFixFactory.createReplaceInaccessibleFieldWithGetterSetterFix

to replace direct access to getter/setter.

Thank you for help!

0

Please sign in to leave a comment.