Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Resolves #5
This should work for most exposed java functionality. Getting code completion for python is more complicated due to dynamic typing.
I first tried to port the existing Jython code from the ghidra repository. But this ultimately failed because the ghidra python2 code just didn't work even after porting it to valid python3. Furthermore, the java introspection side relied on jython features, which were also hard to port over.
So I gave up and wrote a new implementation. IMHO this code is more readable and robust, because it uses the python standard library to achieve most of it and no python magic and doesn't use
eval()
everywhere. It mostly traverses the AST generated by python. The only case this code useseval
for is getting properties from local scope.Still, a lot of the boilerplate on the Java side was copied over (and adapted) from the ghidra implementation, such as code completion colors.
How does this work? (Not a complete overview; only the important points)
The code is split in two parts. A Python side
complete.py
:ast.parse
eval
inspect.getmembers
for a list of all members and find the one we wantPythonCodeCompletionFactory.getReturnType
, which returns a faked java object of the return type. More on that later oninspect.getmembers
and turn them into code completions. If we have a member typed out (e.g.currentProgram().getM
), we filter this list by a prefix.The Java side mostly resolves around implementing the "faked" objects returned by
PythonCodeCompletionFactory.getReturnType
.We use a private inner class
InspectableJavaObject<T>
, which holds a reference to thejava.lang.Class
of the object we want to fake. We can then get a property list viagetProperties()
, these return either moreInspectableJavaObject
s for fields or another typeInspectableJavaMethod
for functions defined in that class. TheInspectableJavaMethod
needs a special case in thegetReturnType
implementation that resolves the Java Class of the object type we wanted to fake instead.Additionally due to a jep limitation, an interpreter may only be used from the same thread it was created on. Therefore I had to move the interpreter to its own Java Thread. Any action to run on the interpreter can now be done by submitting Futures and waiting for their completion.
I also got rid of the
exec
call injepwrappers.py
as this made it hard to get the type information into the signature of the wrapper functions.Limitations
complete.py
uses a lot of match statements for traversing the AST. Unfortunately match statements are a relatively new addition to python only being added in 3.10. However, typing this out withif ... else
chains is a nightmare. If this is a problem, we could fall back to no autocompletion in case the module cannot be loaded.Furthermore, there is a special case for the
str
type. Jep converts between some Java and Python types automatically. This is mostly observed onjava.lang.String
<->str
So when the java side reports that a function returns
java.lang.String
, the python side has to fix the type tostr
or else the completions are wrong. While this isn't pretty I'm not sure if there is a better solution.The same problem applies to other types as well. However I fear the other cases are not as straight forward as this one. Additionally the string problem is the most common one, so this should work fine for most cases.
The Python
locals()
function cannot be evaluated directly from the java side, as it must be run from inside python. Unfortunately this is needed to get the local variables. The workaround is currently to eval and assign it to a variable that can be retrieved later, which is ugly because it pollutes the script environment (and needs a special case not to break the autocompletion)As we are trying to parse the command character by character the completion runs in
O(n^2)
time, which could be a problem for longer inputs. For testing I tested completion on the string"foo("*500 + "currentProgram().getM"
and completion results still completed instantly so this hopefully shouldn't be a problem. This string is already too long for the interpreter console anyways.Completion on the python side are limited mostly by signatures on python functions. Therefore it is quite important that the builtin wrapper function return values are correctly typed or else code completion will not work on them.
Additionally, this code does not give any hints on the argument types or the number of arguments. This may be nontrivial though, especially considering the wrapper functions have no sensible function signature arguments, but accept any args.
Furthermore, this only covers the basic completion cases. More challenging cases such as array subscripts and import autocompletions are not covered at all.