blob: 6421acfd5b255ce6cbc7aa5441fb26cd5d3fb761 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import org.eclipse.jdt.core.CompletionRequestorAdapter;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IType;
import org.eclipse.jdt.core.ITypeHierarchy;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.internal.corext.Assert;
import org.eclipse.jdt.internal.corext.util.SuperTypeHierarchyCache;
import org.eclipse.jdt.internal.ui.util.StringMatcher;
/**
* This class triggers a code-completion that will track all local ane member variables for later
* use as a parameter guessing proposal.
*
* @author Andrew McCullough
*/
public class ParameterGuesser {
private static final class Variable {
/**
* Variable type. Used to choose the best guess based on scope (Local beats instance beats inherited)
*/
public static final int LOCAL= 0;
public static final int FIELD= 1;
public static final int INHERITED_FIELD= 2;
public final String typePackage;
public final String typeName;
public final String name;
public final int variableType;
public final int positionScore;
public boolean alreadyMatched;
/**
* Creates a variable.
*/
public Variable(String typePackage, String typeName, String name, int variableType, int positionScore) {
this.typePackage= typePackage;
this.typeName= typeName;
this.name= name;
this.variableType= variableType;
this.positionScore= positionScore;
}
/*
* @see Object#toString()
*/
public String toString() {
StringBuffer buffer= new StringBuffer();
if (typePackage.length() != 0) {
buffer.append(typePackage);
buffer.append('.');
}
buffer.append(typeName);
buffer.append(' ');
buffer.append(name);
buffer.append(" ("); //$NON-NLS-1$
buffer.append(variableType);
buffer.append(')');
return buffer.toString();
}
}
private static final class VariableCollector extends CompletionRequestorAdapter {
/** The enclosing type name */
private String fEnclosingTypeName;
/** The local and member variables */
private List fVariables;
public List collect(int codeAssistOffset, ICompilationUnit compilationUnit) throws JavaModelException {
Assert.isTrue(codeAssistOffset >= 0);
Assert.isNotNull(compilationUnit);
fVariables= new ArrayList();
String source= compilationUnit.getSource();
if (source == null)
return fVariables;
fEnclosingTypeName= getEnclosingTypeName(codeAssistOffset, compilationUnit);
// find some whitepace to start our variable-finding code complete from.
// this allows the VariableTracker to find all available variables (no prefix to match for the code completion)
int completionOffset= getCompletionOffset(source, codeAssistOffset);
compilationUnit.codeComplete(completionOffset, this);
return fVariables;
}
private static String getEnclosingTypeName(int codeAssistOffset, ICompilationUnit compilationUnit) throws JavaModelException {
IJavaElement element= compilationUnit.getElementAt(codeAssistOffset);
if (element == null)
return null;
element= element.getAncestor(IJavaElement.TYPE);
if (element == null)
return null;
return element.getElementName();
}
private static int getCompletionOffset(String source, int start) {
int index= start;
while (index > 0 && !Character.isWhitespace(source.charAt(index - 1)))
index--;
return index;
}
/**
* Determine if the declaring type matches the type of the code completion invokation
*/
private final boolean isInherited(String declaringTypeName) {
return !declaringTypeName.equals(fEnclosingTypeName);
}
private void addVariable(int varType, char[] typePackageName, char[] typeName, char[] name) {
fVariables.add(new Variable(new String(typePackageName), new String(typeName), new String(name), varType, fVariables.size()));
}
/*
* @see ICompletionRequestor#acceptField(char[], char[], char[], char[], char[], char[], int, int, int, int)
*/
public void acceptField(char[] declaringTypePackageName, char[] declaringTypeName, char[] name,
char[] typePackageName, char[] typeName, char[] completionName, int modifiers, int completionStart,
int completionEnd, int relevance)
{
if (!isInherited(new String(declaringTypeName)))
addVariable(Variable.FIELD, typePackageName, typeName, name);
else
addVariable(Variable.INHERITED_FIELD, typePackageName, typeName, name);
}
/*
* @see ICompletionRequestor#acceptLocalVariable(char[], char[], char[], int, int, int, int)
*/
public void acceptLocalVariable(char[] name, char[] typePackageName, char[] typeName, int modifiers,
int completionStart, int completionEnd, int relevance)
{
addVariable(Variable.LOCAL, typePackageName, typeName, name);
}
}
/** The compilation unit we are computing the completion for */
private final ICompilationUnit fCompilationUnit;
/** The code assist offset. */
private final int fCodeAssistOffset;
/** Local and member variables of the compilation unit */
private List fVariables;
/**
* Creates a parameter guesser for compilation unit and offset.
*
* @param codeAssistOffset the offset at which to perform code assist
* @param compilationUnit the compilation unit in which code assist is performed
*/
public ParameterGuesser(int codeAssistOffset, ICompilationUnit compilationUnit) {
Assert.isTrue(codeAssistOffset >= 0);
Assert.isNotNull(compilationUnit);
fCodeAssistOffset= codeAssistOffset;
fCompilationUnit= compilationUnit;
}
/**
* Returns the offset at which code assist is performed.
*/
public int getCodeAssistOffset() {
return fCodeAssistOffset;
}
/**
* Returns the compilation unit in which code assist is performed.
*/
public ICompilationUnit getCompilationUnit() {
return fCompilationUnit;
}
/**
* Returns the name of the variable or field that best matches the type and name of the argument.
*
* @param paramPackage - the package of the parameter we are trying to match
* @param paramType - the qualified name of the parameter we are trying to match
* @param paramName - the name of the paramater (used to find similarly named matches)
* @return returns the name of the best match, or <code>null</code> if no match found
*/
public String guessParameterName(String paramPackage, String paramType, String paramName) throws JavaModelException {
if (fVariables == null) {
VariableCollector variableCollector= new VariableCollector();
fVariables= variableCollector.collect(fCodeAssistOffset, fCompilationUnit);
}
List typeMatches= findFieldsMatchingType(fVariables, paramPackage, paramType);
return chooseBestMatch(typeMatches, paramName);
}
/**
* Determine the best match of all possible type matches. The input into this method is all
* possible completions that match the type of the argument. The purpose of this method is to
* choose among them based on the following simple rules:
*
* 1) Local Variables > Instance/Class Variables > Inherited Instance/Class Variables
*
* 2) A longer case insensitive substring match will prevail
*
* 3) A better source position score will prevail (the declaration point of the variable, or
* "how close to the point of completion?"
*
* 4) Variables that have not been used already during this completion will prevail over
* those that have already been used (this avoids the same String/int/char from being passed
* in for multiple arguments)
*
* @return returns <code>null</code> if no match is found
*/
private static String chooseBestMatch(List typeMatches, String paramName) {
if (typeMatches == null)
return null;
Variable bestMatch= null;
int bestSubstringScore= 0;
for (Iterator i= typeMatches.iterator(); i.hasNext(); ) {
Variable variable= (Variable) i.next();
if (variable.alreadyMatched)
continue;
int subStringScore= getLongestCommonSubstring(variable.name, paramName).length();
if (bestMatch == null) {
bestMatch= variable;
bestSubstringScore= subStringScore;
} else if (variable.variableType < bestMatch.variableType) {
bestMatch= variable;
} else if (subStringScore > bestSubstringScore) {
bestMatch= variable;
bestSubstringScore= subStringScore;
} else if (variable.positionScore > bestMatch.positionScore) {
bestMatch= variable;
} else if (bestMatch.alreadyMatched && !variable.alreadyMatched) {
bestMatch= variable;
}
}
if (bestMatch == null)
return null;
bestMatch.alreadyMatched= true;
return bestMatch.name;
}
/**
* Finds a local or member variable that matched the type of the parameter
*/
private List findFieldsMatchingType(List variables, String typePackage, String typeName) throws JavaModelException {
if (typeName == null || typeName.length() == 0)
return null;
// traverse the lists in reverse order, since it is empirically true that the code
// completion engine returns variables in the order they are found -- and we want to find
// matches closest to the codecompletion point.. No idea if this behavior is guaranteed.
List matches= new ArrayList();
for (ListIterator iterator= variables.listIterator(variables.size()); iterator.hasPrevious(); ) {
Variable variable= (Variable) iterator.previous();
if (isTypeMatch(variable, typePackage, typeName))
matches.add(variable);
}
return matches.isEmpty() ? null : matches;
}
/**
* Return true if variable is a match for the given type. This method will search the SuperTypeHierarchy
* of the vairable's type and see if the argument's type is included. This method is approximately
* the same as: if (argumentVar.getClass().isAssignableFrom(field.getClass()))
*/
private boolean isTypeMatch(Variable variable, String typePackage, String typeName) throws JavaModelException {
// this should look at fully qualified name, but currently the ComepletionEngine is not
// sending the packag names...
// if there is no package specified, do the check on type name only. This will work for primitives
// and for local variables that cannot be resolved.
if (typePackage == null || variable.typePackage == null ||
typePackage.length() == 0 || variable.typePackage.length() == 0) {
if (variable.typeName.equals(typeName))
return true;
}
// if we get to here, we're doing a "fully qualified match" -- meaning including packages, no primitives
// and no unresolved variables.
// if there is an exact textual match, there is no need to search type hierarchy.. this is
// a quick way to pick up an exact match.
if (variable.typeName.equals(typeName) && variable.typePackage.equals(typePackage))
return true;
// otherwise, we get a match/nomatch by searching the type hierarchy
return isAssignable(variable, typePackage, typeName);
}
/**
* Returns true if variable is assignable to a type, false otherwise.
*/
private boolean isAssignable(Variable variable, String typePackage, String typeName) throws JavaModelException {
// check for an exact match (fast)
StringBuffer paramTypeName= new StringBuffer();
if (typePackage.length() != 0) {
paramTypeName.append(typePackage);
paramTypeName.append('.');
}
paramTypeName.append(typeName);
StringBuffer varTypeName= new StringBuffer();
if (variable.typePackage.length() != 0) {
varTypeName.append(variable.typePackage);
varTypeName.append('.');
}
varTypeName.append(variable.typeName);
IJavaProject project= fCompilationUnit.getJavaProject();
IType paramType= project.findType(paramTypeName.toString());
IType varType= project.findType(varTypeName.toString());
if (varType == null || paramType == null)
return false;
ITypeHierarchy hierarchy= SuperTypeHierarchyCache.getTypeHierarchy(varType);
return hierarchy.contains(paramType);
}
/**
* Returns the longest common substring of two strings.
*/
private static String getLongestCommonSubstring(String first, String second) {
String shorter= (first.length() <= second.length()) ? first : second;
String longer= shorter == first ? second : first;
int minLength= shorter.length();
StringBuffer pattern= new StringBuffer(shorter.length() + 2);
String longestCommonSubstring= ""; //$NON-NLS-1$
for (int i= 0; i < minLength; i++) {
for (int j= i + 1; j <= minLength; j++) {
if (j - i < longestCommonSubstring.length())
continue;
String substring= shorter.substring(i, j);
pattern.setLength(0);
pattern.append('*');
pattern.append(substring);
pattern.append('*');
StringMatcher matcher= new StringMatcher(pattern.toString(), true, false);
if (matcher.match(longer))
longestCommonSubstring= substring;
}
}
return longestCommonSubstring;
}
}