org.codehaus.groovy.tools.shell.completion.ReflectionCompletor.groovy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of groovy-all Show documentation
Show all versions of groovy-all Show documentation
Groovy: A powerful, dynamic language for the JVM
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.tools.shell.completion
import org.codehaus.groovy.antlr.GroovySourceToken
import org.codehaus.groovy.control.MultipleCompilationErrorsException
import org.codehaus.groovy.runtime.InvokerHelper
import org.codehaus.groovy.tools.shell.Groovysh
import org.codehaus.groovy.tools.shell.util.Preferences
import org.fusesource.jansi.Ansi
import org.fusesource.jansi.AnsiRenderer
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.util.regex.Pattern
import static org.codehaus.groovy.antlr.parser.GroovyTokenTypes.*
/**
* Completes fields and methods of Classes or instances.
* Does not quite respect the contract of IdentifierCompletor, as last Token may be a dot or not,
* thus also returns as int the cursor position.
*/
class ReflectionCompletor {
private static final NavigablePropertiesCompleter PROPERTIES_COMPLETER = new NavigablePropertiesCompleter()
private static final Pattern BEAN_ACCESSOR_PATTERN = ~'^(get|set|is)[A-Z].*'
final Groovysh shell
/**
*
* @param shell
* @param metaclass_completion_prefix_length how long the prefix must be to disaply candidates from metaclass
*/
ReflectionCompletor(final Groovysh shell) {
this.shell = shell
}
int complete(final List tokens, final List candidates) {
GroovySourceToken currentElementToken = null
GroovySourceToken dotToken
List previousTokens
if (tokens.size() < 2) {
throw new IllegalArgumentException('must be invoked with at least 2 tokens, one of which is dot' + tokens*.text)
}
if (tokens.last().type == DOT || tokens.last().type == OPTIONAL_DOT || tokens.last().type == SPREAD_DOT) {
dotToken = tokens.last()
previousTokens = tokens[0..-2]
} else {
if (tokens[-2].type != DOT && tokens[-2].type != OPTIONAL_DOT && tokens[-2].type != SPREAD_DOT) {
throw new IllegalArgumentException('must be invoked with token list with dot at last position or one position before' + tokens*.text)
}
currentElementToken = tokens.last()
dotToken = tokens[-2]
previousTokens = tokens[0..-3]
}
Object instanceOrClass = getInvokerClassOrInstance(previousTokens)
if (instanceOrClass == null) {
return -1
}
if (dotToken.type == SPREAD_DOT) {
/**
* for aggregate types, find an arbitrary collection-member
* element within the instance. This may cause invalid completion candidates when the collection is not
* homogenous, but still better than no completion at all. Alternatively the union or intersection of
* candidate completions could be built. For non-aggregate types, we assume that whatever find()
* returns is useful for *. completion as well.
*/
instanceOrClass = instanceOrClass.find()
if (instanceOrClass == null) {
return -1
}
}
String identifierPrefix
if (currentElementToken) {
identifierPrefix = currentElementToken.text
} else {
identifierPrefix = ''
}
return completeInstanceMembers(instanceOrClass, identifierPrefix, candidates, currentElementToken, dotToken)
}
private int completeInstanceMembers(final Object instanceOrClass,
final String identifierPrefix,
final List candidates,
final GroovySourceToken currentElementToken,
final GroovySourceToken dotToken) {
// look for public methods/fields that match the prefix
Collection myCandidates = getPublicFieldsAndMethods(instanceOrClass, identifierPrefix)
boolean showAllMethods = (identifierPrefix.length() >= Integer.valueOf(Preferences.get(Groovysh.METACLASS_COMPLETION_PREFIX_LENGTH_PREFERENCE_KEY, '3')))
// Also add metaclass methods if prefix is long enough (user would usually not care about those)
myCandidates.addAll(getMetaclassMethods(
instanceOrClass,
identifierPrefix,
showAllMethods).collect({ String it -> new ReflectionCompletionCandidate(it) }))
if (!showAllMethods) {
// user probably does not care to see default Object / GroovyObject Methods,
// they obfuscate the business logic
removeStandardMethods(myCandidates)
}
// specific DefaultGroovyMethods only suggested for suitable instances
myCandidates.addAll(getDefaultMethods(instanceOrClass,
identifierPrefix).collect({ String it -> new ReflectionCompletionCandidate(it, AnsiRenderer.Code.BLUE.name()) }))
if (myCandidates.size() > 0) {
myCandidates = myCandidates.sort()
if (Boolean.valueOf(Preferences.get(Groovysh.COLORS_PREFERENCE_KEY, 'true'))) {
candidates.addAll(myCandidates.collect(
{ ReflectionCompletionCandidate it ->
AnsiRenderer.render(it.value,
it.jAnsiCodes.toArray(new String[it.jAnsiCodes.size()]))
}))
} else {
candidates.addAll(myCandidates*.value)
}
int lastDot
// dot could be on previous line
if (currentElementToken && dotToken.line != currentElementToken.line) {
lastDot = currentElementToken.column - 1
} else {
// Spread-dot has length 2!
lastDot = dotToken.column +(dotToken.getText().length() - 1)
}
return lastDot
}
// no candidates
return -1
}
/**
* Takes the last ? tokens of the list that form a simple expression,
* evaluates it and returns a result. "Simple" means evaluation is known to be
* side-effect free.
*/
Object getInvokerClassOrInstance(final List groovySourceTokens) {
if (!groovySourceTokens
|| groovySourceTokens.last().type == DOT
|| groovySourceTokens.last().type == OPTIONAL_DOT) {
// we expect the list of tokens before a dot.
return null
}
// first, try to detect a sequence of token before the dot that can safely be evaluated.
List invokerTokens = getInvokerTokens(groovySourceTokens)
if (invokerTokens) {
try {
String instanceRefExpression = tokenListToEvalString(invokerTokens)
instanceRefExpression = instanceRefExpression.replace('\n', '')
Object instance = shell.interp.evaluate([shell.getImportStatements()] + ['true'] + [instanceRefExpression])
return instance
} catch (MissingPropertyException |
MissingMethodException |
MissingFieldException |
MultipleCompilationErrorsException e) {
}
}
return null
}
/**
* return the last tokens of a list that form an expression to be completed after the next dot, or null if
* expression cannot be detected. This discards Expressions that could easily have side effects or be long
* in evaluation. However it assumes that operators can be evaluated without side-effect or long running
* operation. Users who use operators for which this does not hold should not use tab completion.
* @param groovySourceTokens
* @return
*/
static List getInvokerTokens(final List groovySourceTokens) {
int validIndex = groovySourceTokens.size()
if (validIndex == 0) {
return []
}
// implementation goes backwards on token list, adding strings
// to be evaluated later
// need to collect using Strings, to support evaluation of string literals
Stack expectedOpeners = new Stack()
GroovySourceToken lastToken = null
outerloop:
for (GroovySourceToken loopToken in groovySourceTokens.reverse()) {
switch (loopToken.type) {
// a combination of any of these can be evaluated without side effects
// this just avoids any parentheses,
// could maybe be extended further if harmless parentheses can be detected .
// This allows already a lot of powerful simple completions, like [foo: Baz.bar]['foo'].
case STRING_LITERAL:
// must escape String for evaluation, need the original string e.g. for mapping key
break
case LPAREN:
if (expectedOpeners.empty()) {
break outerloop
}
if (expectedOpeners.pop() != LPAREN) {
return []
}
break
case LBRACK:
if (expectedOpeners.empty()) {
break outerloop
}
if (expectedOpeners.pop() != LBRACK) {
return []
}
break
case RBRACK:
expectedOpeners.push(LBRACK)
break
case RPAREN:
expectedOpeners.push(LPAREN)
break
// tokens which indicate we have reached the beginning of a statement
// operator tokens (must not be evaluated, as they can have side effects via evil overriding
case COMPARE_TO:
case EQUAL:
case NOT_EQUAL:
case ASSIGN:
case GT:
case LT:
case GE:
case LE:
case PLUS:
case PLUS_ASSIGN:
case MINUS:
case MINUS_ASSIGN:
case STAR:
case STAR_ASSIGN:
case DIV:
case DIV_ASSIGN:
case BOR:
case BOR_ASSIGN:
case BAND:
case BAND_ASSIGN:
case BXOR:
case BXOR_ASSIGN:
case BNOT:
case LOR:
case LAND:
case LNOT:
case LITERAL_in:
case LITERAL_instanceof:
if (expectedOpeners.empty()) {
break outerloop
}
break
// tokens which indicate we have reached the beginning of a statement
case LCURLY:
case SEMI:
case STRING_CTOR_START:
break outerloop
// tokens we accept
case IDENT:
if (lastToken) {
if (lastToken.type == LPAREN) {
//Method invocation,must be avoided
return []
}
if (lastToken.type == IDENT) {
// could be attempt to invoke closure like 'foo.each bar.baz'
return []
}
}
break
// may begin expression when outside brackets (from back)
case RANGE_INCLUSIVE:
case RANGE_EXCLUSIVE:
case COLON:
case COMMA:
if (expectedOpeners.empty()) {
break outerloop
}
// harmless literals
case LITERAL_true:
case LITERAL_false:
case NUM_INT:
case NUM_FLOAT:
case NUM_LONG:
case NUM_DOUBLE:
case NUM_BIG_INT:
case NUM_BIG_DECIMAL:
case MEMBER_POINTER:
case DOT:
case OPTIONAL_DOT:
break
default:
return null
} // end switch
validIndex --
lastToken = loopToken
} // end for
return groovySourceTokens[(validIndex)..-1]
}
static String tokenListToEvalString(final List groovySourceTokens) {
StringBuilder builder = new StringBuilder()
for (GroovySourceToken token: groovySourceTokens) {
if (token.type == STRING_LITERAL) {
builder.append('\'').append(token.text).append('\'')
} else {
builder.append(token.text)
}
}
return builder.toString()
}
static boolean acceptName(final String name, final String prefix) {
return (!prefix || name.startsWith(prefix)) &&
(!(name.contains('$')) && !(name.startsWith('_')))
}
static Collection getMetaclassMethods(final Object instance, final String prefix, final boolean includeMetaClassImplMethods) {
Set rv = new HashSet()
MetaClass metaclass = InvokerHelper.getMetaClass(instance)
if (includeMetaClassImplMethods || !(metaclass instanceof MetaClassImpl)) {
metaclass.metaMethods.each { MetaMethod mmit ->
if (acceptName(mmit.name, prefix)) {
rv << mmit.getName() + (mmit.parameterTypes.length == 0 ? '()' : '(')
}
}
}
return rv.sort()
}
/**
* Build a list of public fields and methods for an object
* that match a given prefix.
* @param instance the object
* @param prefix the prefix that must be matched
* @return the list of public methods and fields that begin with the prefix
*/
static Collection getPublicFieldsAndMethods(final Object instance, final String prefix) {
Set rv = new HashSet()
Class clazz = instance.getClass()
if (clazz == null) {
return rv
}
boolean isClass = (clazz == Class)
if (isClass) {
clazz = instance as Class
}
Class loopclazz = clazz
// render immediate class members bold when completing an instance
boolean renderBold = ! isClass
// hide static members for instances unless user typed a prefix
boolean showStatic = isClass || (prefix.length() >= Integer.valueOf(Preferences.get(Groovysh.METACLASS_COMPLETION_PREFIX_LENGTH_PREFERENCE_KEY, '3')))
while (loopclazz != null && loopclazz != Object && loopclazz != GroovyObject) {
addClassFieldsAndMethods(loopclazz, showStatic, !isClass, prefix, rv, renderBold)
renderBold = false
loopclazz = loopclazz.superclass
}
if (clazz.isArray() && !isClass) {
// Arrays are special, these public members cannot be found via Reflection
for (String member : ['length', 'clone()']) {
if (member.startsWith(prefix)) {
rv.add(new ReflectionCompletionCandidate(member, Ansi.Attribute.INTENSITY_BOLD.name()))
}
}
}
// other completions that are commonly possible with properties
if (!isClass) {
Set candidates = new HashSet()
PROPERTIES_COMPLETER.addCompletions(instance, prefix, candidates)
rv.addAll(candidates.collect({String it -> new ReflectionCompletionCandidate(it, AnsiRenderer.Code.MAGENTA.name())}))
}
return rv.sort()
}
/**
* removes candidates that, most of the times, a programmer does not want to see in completion
* @param candidates
*/
static removeStandardMethods(final Collection candidates) {
for (String defaultMethod : [
'clone()', 'finalize()', 'getClass()',
'getMetaClass()', 'getProperty(', 'invokeMethod(', 'setMetaClass(', 'setProperty(',
'equals(', 'hashCode()', 'toString()',
'notify()', 'notifyAll()', 'wait(', 'wait()']) {
for (ReflectionCompletionCandidate candidate : candidates) {
if (defaultMethod.equals(candidate.value)) {
candidates.remove(candidate)
break
}
}
}
}
/**
* Offering all DefaultGroovyMethods on any object is too verbose, hiding all
* removes user-friendliness. So here util methods will be added to candidates
* if the instance is of a suitable type.
* This does not need to be strictly complete, only the most useful functions may appear.
*/
static List getDefaultMethods(final Object instance, final String prefix) {
List candidates = []
if (instance instanceof Iterable) {
[
'any()', 'any(',
'collect()', 'collect(',
'combinations()',
'count(',
'countBy(',
'drop(', 'dropRight(', 'dropWhile(',
'each()', 'each(',
'eachPermutation(',
'every()', 'every(',
'find(', 'findResult(', 'findResults(',
'flatten()',
'init()',
'inject(',
'intersect(',
'join(',
'max()', 'min()',
'reverse()',
'size()',
'sort()',
'split(',
'take(', 'takeRight(', 'takeWhile(',
'toSet()',
'retainAll(', 'removeAll(',
'unique()', 'unique('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
if (instance instanceof Collection) {
[
'grep('
].findAll({ it.startsWith(prefix) }).each({ candidates.add(it) })
}
if (instance instanceof List) {
[
'collate(',
'execute()', 'execute(',
'pop()',
'transpose()'
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
}
if (instance instanceof Map) {
[
'any(',
'collect(',
'collectEntries(',
'collectMany(',
'count(',
'drop(',
'each(',
'every(',
'find(', 'findAll(', 'findResult(', 'findResults(',
'groupEntriesBy(', 'groupBy(',
'inject(', 'intersect(',
'max(', 'min(',
'sort(',
'spread()',
'subMap(',
'take(', 'takeWhile('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof File) {
[
'append(',
'createTempDir()', 'createTempDir(',
'deleteDir()', 'directorySize()',
'eachByte(', 'eachDir(', 'eachDirMatch(', 'eachDirRecurse(', 'eachFile(', 'eachFileMatch(', 'eachFileRecurse(', 'eachLine(',
'filterLine(',
'getBytes()', 'getText()', 'getText(',
'newInputStream()', 'newOutputStream()', 'newPrintWriter()', 'newPrintWriter(', 'newReader()', 'newReader(', 'newWriter()', 'newWriter(',
'readBytes()', 'readLines(',
'setBytes(', 'setText(', 'size()', 'splitEachLine(',
'traverse(',
'withInputStream(', 'withOutputStream(', 'withPrintWriter(', 'withReader(', 'withWriter(', 'withWriterAppend(', 'write('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof String) {
[
'capitalize()', 'center(', 'collectReplacements(', 'count(',
'decodeBase64()', 'decodeHex()', 'denormalize()',
'eachLine(', 'eachMatch(', 'execute()', 'execute(',
'find(', 'findAll(',
'isAllWhitespace()', 'isBigDecimal()', 'isBigInteger()', 'isDouble()', 'isFloat()', 'isInteger()', 'isLong()', 'isNumber()',
'normalize()',
'padLeft(', 'padRight(',
'readLines()', 'reverse()',
'size()', 'splitEachLine(', 'stripIndent(', 'stripMargin(',
'toBigDecimal()', 'toBigInteger()', 'toBoolean()', 'toCharacter()', 'toDouble()', 'toFloat()', 'toInteger()',
'toList()', 'toLong()', 'toSet()', 'toShort()', 'toURI()', 'toURL()',
'tokenize(', 'tr('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof URL) {
[
'eachLine(',
'filterLine(',
'getBytes()', 'getBytes(', 'getText()', 'getText(',
'newInputStream()', 'newInputStream(', 'newReader()', 'newReader(',
'readLines()', 'readLines(',
'splitEachLine(',
'withInputStream(', 'withReader('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof InputStream) {
[
'eachLine(',
'filterLine(',
'getBytes()', 'getText()', 'getText(',
'newReader()', 'newReader(',
'readLines()', 'readLines(',
'splitEachLine(',
'withReader(', 'withStream('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof OutputStream) {
[
'newPrintWriter()', 'newWriter()', 'newWriter(',
'setBytes(',
'withPrintWriter(', 'withStream(', 'withWriter('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
if (instance instanceof Number) {
[
'abs()',
'downto(',
'times(',
'power(',
'upto('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
Class clazz = instance.getClass()
if (clazz != null && clazz != Class && clazz.isArray()) {
[
'any()', 'any(',
'collect()', 'collect(',
'count(',
'countBy(',
'drop(', 'dropRight(', 'dropWhile(',
'each()', 'each(',
'every()', 'every(',
'find(', 'findResult(',
'flatten()',
'init()',
'inject(',
'join(',
'max()', 'min()',
'reverse()',
'size()',
'sort()',
'split(',
'take(', 'takeRight(', 'takeWhile('
].findAll({it.startsWith(prefix)}).each({candidates.add(it)})
}
return candidates
}
private static Collection addClassFieldsAndMethods(final Class clazz,
final boolean includeStatic,
final boolean includeNonStatic,
final String prefix,
final Collection rv,
final boolean renderBold) {
Field[] fields = (includeStatic && ! includeNonStatic) ? clazz.fields : clazz.getDeclaredFields()
fields.each { Field fit ->
if (acceptName(fit.name, prefix)) {
int modifiers = fit.getModifiers()
if (Modifier.isPublic(modifiers) && (Modifier.isStatic(modifiers) ? includeStatic : includeNonStatic)) {
if (!clazz.isEnum()
|| !(!includeStatic && Modifier.isPublic(modifiers) && Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers) && fit.type == clazz)) {
ReflectionCompletionCandidate candidate = new ReflectionCompletionCandidate(fit.name)
if (!Modifier.isStatic(modifiers)) {
if (renderBold) {
candidate.jAnsiCodes.add(Ansi.Attribute.INTENSITY_BOLD.name())
}
}
rv << candidate
}
}
}
}
Method[] methods = (includeStatic && ! includeNonStatic) ? clazz.methods : clazz.getDeclaredMethods()
for (Method methIt : methods) {
String name = methIt.getName()
if (name.startsWith("super\$")) {
name = name.substring(name.find("^super\\\$.*\\\$").length())
}
int modifiers = methIt.getModifiers()
if (Modifier.isPublic(modifiers) && (Modifier.isStatic(modifiers) ? includeStatic : includeNonStatic)) {
boolean fieldnameSuggested = false
// bean fieldname can be used instead of accessor, tidies up completion candidates
// the same goes for static fields // accessors
if (name.matches(BEAN_ACCESSOR_PATTERN)) {
String fieldname = getFieldnameForAccessor(name, methIt.parameterTypes.length)
if (fieldname != null && fieldname != 'metaClass' && fieldname != 'property') {
if (acceptName(fieldname, prefix)) {
fieldnameSuggested = true
ReflectionCompletionCandidate fieldCandidate = new ReflectionCompletionCandidate(fieldname)
if (!rv.contains(fieldCandidate)) {
if (!Modifier.isStatic(modifiers) && renderBold) {
fieldCandidate.jAnsiCodes.add(Ansi.Attribute.INTENSITY_BOLD.name())
}
rv.add(fieldCandidate)
}
}
}
}
if (! fieldnameSuggested && acceptName(name, prefix)) {
ReflectionCompletionCandidate candidate = new ReflectionCompletionCandidate(name + (methIt.parameterTypes.length == 0 ? '()' : '('))
if (!Modifier.isStatic(modifiers) && renderBold) {
candidate.jAnsiCodes.add(Ansi.Attribute.INTENSITY_BOLD.name())
}
rv.add(candidate)
}
}
}
for (Class interface_ : clazz.getInterfaces()) {
addClassFieldsAndMethods(interface_, includeStatic, includeNonStatic, prefix, rv, false)
}
}
static CharSequence getFieldnameForAccessor(String accessor, int parameterLength) {
String fieldname = null
if (accessor.startsWith('get')) {
if (parameterLength == 0) {
fieldname = accessor.substring(3)
}
} else if (accessor.startsWith('set')) {
if (parameterLength == 1) {
fieldname = accessor.substring(3)
}
} else if (accessor.startsWith('is')) {
if (parameterLength == 0) {
fieldname = accessor.substring(2)
}
} else {
throw new IllegalStateException('getFieldnameForAccessor called with invalid accessor : ' + accessor)
}
if (fieldname == null) {
return null
}
return fieldname[0].toLowerCase() + fieldname.substring(1)
}
}