blob: c1cdf7a29795350b6e043aa63d79ae9e9897d638 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2006 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.corext.refactoring.rename;
import com.ibm.icu.text.BreakIterator;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.internal.ui.text.JavaWordIterator;
/**
* This class contains methods for suggesting new names for variables or methods
* whose name consists at least partly of the name of their declaring type (or
* in case of methods, the return type or a parameter type).
*
* The methods return the newly suggested method or variable name in case of a
* match, or null in case nothing matched.
*
* In any case, prefixes and suffixes are removed from variable names. As method
* names have no configurable suffixes or prefixes, they are left unchanged. The
* remaining name is called "stripped element name".
*
* After the match according to the strategy, prefixes and suffixes are
* reapplied to the names.
*
* EXACT STRATEGY (always performed).
* ----------------------------------------------------------------
*
* The stripped element name is directly compared with the type name:
*
* a) the first character must match case-insensitive
*
* b) all other characters must match case-sensitive
*
* In case of a match, the new type name is returned (first character adapted,
* respectively). Suffixes/Prefixes are reapplied.
*
* Note that this also matches fields with names like "SomeField", "fsomeField",
* and method names like "JavaElement()".
*
* EMBEDDED STRATEGY (performed second if chosen by user).
* ----------------------------------------------------------------
*
* A search is performed in the stripped element name for the old type name:
*
* a) the first character must match case-insensitive
*
* b) all other characters must match case-sensitive
*
* c) the stripped element name must end after the type name, or the next
* character must be a non-letter, or the next character must be upper cased.
*
* In case of a match, the new type is inserted into the stripped element name,
* replacing the old type name, first character adapted to the correct case.
* Suffixes/Prefixes are reapplied.
*
* Note that this also matches methods with names like "createjavaElement()" or
* fields like "fjavaElementCache".
*
* SUFFIX STRATEGY (performed third if chosen by user)
* ----------------------------------------------------------------
*
* The new and old type names are analyzed for "camel case suffixes", that is,
* substrings which begin with an uppercased letter. For example,
* "SimpleJavaElement" is split into the three hunks "Simple",
* "Java", and "Element". If one type name has more suffixes than the
* other, both are stripped to the smaller size.
*
* Then, a search is performed in the stripped variable name hunks from back to
* front. At least the last hunk must be found, others may then extend the match.
* Each hunk must match like in the exact strategy, i.e.
*
* a) the first character must match case-insensitive
*
* b) all other characters must match case-sensitive
*
* In case of a match, the matched hunks of the new type replace
* the hunks of the old type. Suffixes/Prefixes are reapplied.
*
* Note that numbers and other non-letter characters belong to the previous
* camel case substring.
*
*
* @since 3.2
*
*/
public class RenamingNameSuggestor {
/*
* ADDITIONAL OPTIONS
* ----------------------------------------------------------------
*
* There are two additional flags which may be set in this class to allow
* better matching of special cases:
*
* a) Special treatment of leading "I"s in type names, i.e. interface names
* like "IJavaElement". If the corresponding flag is set, leading "I"s are
* stripped from type names if the second char is also uppercase to allow
* exact matching of variable names like "javaElement" for type
* "IJavaElement". Note that embedded matching already matches cases like
* this.
*
* b) Special treatment of all-uppercase type names or all-uppercase type
* name camel-case hunks, i.e. names like "AST" or "PersonalURL". If the
* corresponding flag is set, the type name hunks will be transformed such
* that variables like "fAst", "ast", "personalUrl", or "url" are found as
* well. The target name will be transformed too if it is an
* all-uppercase type name camel-case hunk as well.
*
* NOTE that in exact or embedded mode, the whole type name must be
* all-uppercase to allow matching custom-lowercased variable names, i.e.
* there are no attempts to "guess" which hunk of the new name should be lowercased
* to match a partly lowercased variable name. In suffix mode, hunks of the
* new type which are at the same position as in the old type will be
* lowercased if necessary.
*
* c) Support for (english) plural forms. If the corresponding flag is set, the
* suggestor will try to match variables which have plural forms of the
* type name, for example "handies" for "Handy" or "phones" for "MobilePhone".
* The target name will be transformed as well, i.e. conversion like
* "fHandies" -> "fPhones" are supported.
*
*/
public static final int STRATEGY_EXACT= 1;
public static final int STRATEGY_EMBEDDED= 2;
public static final int STRATEGY_SUFFIX= 3;
private static final String PLURAL_S= "s"; //$NON-NLS-1$
private static final String PLURAL_IES= "ies"; //$NON-NLS-1$
private static final String SINGULAR_Y= "y"; //$NON-NLS-1$
private int fStrategy;
private String[] fFieldPrefixes;
private String[] fFieldSuffixes;
private String[] fStaticFieldPrefixes;
private String[] fStaticFieldSuffixes;
private String[] fLocalPrefixes;
private String[] fLocalSuffixes;
private String[] fArgumentPrefixes;
private String[] fArgumentSuffixes;
private boolean fExtendedInterfaceNameMatching;
private boolean fExtendedAllUpperCaseHunkMatching;
private boolean fExtendedPluralMatching;
public RenamingNameSuggestor() {
this(STRATEGY_SUFFIX);
}
public RenamingNameSuggestor(int strategy) {
Assert.isTrue(strategy >= 1 && strategy <= 3);
fStrategy= strategy;
fExtendedInterfaceNameMatching= true;
fExtendedAllUpperCaseHunkMatching= true;
fExtendedPluralMatching= true;
resetPrefixes();
}
public String suggestNewFieldName(IJavaProject project, String oldFieldName, boolean isStatic, String oldTypeName, String newTypeName) {
initializePrefixesAndSuffixes(project);
if (isStatic)
return suggestNewVariableName(fStaticFieldPrefixes, fStaticFieldSuffixes, oldFieldName, oldTypeName, newTypeName);
else
return suggestNewVariableName(fFieldPrefixes, fFieldSuffixes, oldFieldName, oldTypeName, newTypeName);
}
public String suggestNewLocalName(IJavaProject project, String oldLocalName, boolean isArgument, String oldTypeName, String newTypeName) {
initializePrefixesAndSuffixes(project);
if (isArgument)
return suggestNewVariableName(fArgumentPrefixes, fArgumentSuffixes, oldLocalName, oldTypeName, newTypeName);
else
return suggestNewVariableName(fLocalPrefixes, fLocalSuffixes, oldLocalName, oldTypeName, newTypeName);
}
public String suggestNewMethodName(String oldMethodName, String oldTypeName, String newTypeName) {
Assert.isNotNull(oldMethodName);
Assert.isNotNull(oldTypeName);
Assert.isNotNull(newTypeName);
Assert.isTrue(oldMethodName.length() > 0);
Assert.isTrue(oldTypeName.length() > 0);
Assert.isTrue(newTypeName.length() > 0);
resetPrefixes();
return match(oldTypeName, newTypeName, oldMethodName);
}
public String suggestNewVariableName(String[] prefixes, String[] suffixes, String oldVariableName, String oldTypeName, String newTypeName) {
Assert.isNotNull(prefixes);
Assert.isNotNull(suffixes);
Assert.isNotNull(oldVariableName);
Assert.isNotNull(oldTypeName);
Assert.isNotNull(newTypeName);
Assert.isTrue(oldVariableName.length() > 0);
Assert.isTrue(oldTypeName.length() > 0);
Assert.isTrue(newTypeName.length() > 0);
final String usedPrefix= findLongestPrefix(oldVariableName, prefixes);
final String usedSuffix= findLongestSuffix(oldVariableName, suffixes);
final String strippedVariableName= oldVariableName.substring(usedPrefix.length(), oldVariableName.length() - usedSuffix.length());
String newVariableName= match(oldTypeName, newTypeName, strippedVariableName);
return (newVariableName != null) ? usedPrefix + newVariableName + usedSuffix : null;
}
// -------------------------------------- Match methods
private String match(final String oldTypeName, final String newTypeName, final String strippedVariableName) {
String oldType= oldTypeName;
String newType= newTypeName;
if (fExtendedInterfaceNameMatching && isInterfaceName(oldType) && isInterfaceName(newType)) {
oldType= getInterfaceName(oldType);
newType= getInterfaceName(newType);
}
String newVariableName= matchDirect(oldType, newType, strippedVariableName);
if (fExtendedPluralMatching && newVariableName == null && canPluralize(oldType))
newVariableName= matchDirect(pluralize(oldType), pluralize(newType), strippedVariableName);
return newVariableName;
}
private String matchDirect(String oldType, String newType, final String strippedVariableName) {
/*
* Use all strategies applied by the user. Always start with exact
* matching.
*
* Note that suffix matching may not match the whole type name if the
* new type name has a smaller camel case chunk count.
*/
String newVariableName= exactMatch(oldType, newType, strippedVariableName);
if (newVariableName == null && fStrategy >= STRATEGY_EMBEDDED)
newVariableName= embeddedMatch(oldType, newType, strippedVariableName);
if (newVariableName == null && fStrategy >= STRATEGY_SUFFIX)
newVariableName= suffixMatch(oldType, newType, strippedVariableName);
return newVariableName;
}
private String exactMatch(final String oldTypeName, final String newTypeName, final String strippedVariableName) {
String newName= exactDirectMatch(oldTypeName, newTypeName, strippedVariableName);
if (newName != null)
return newName;
if (fExtendedAllUpperCaseHunkMatching && isUpperCaseCamelCaseHunk(oldTypeName)) {
String oldTN= getFirstUpperRestLowerCased(oldTypeName);
String newTN= isUpperCaseCamelCaseHunk(newTypeName) ? getFirstUpperRestLowerCased(newTypeName) : newTypeName;
newName= exactDirectMatch(oldTN, newTN, strippedVariableName);
}
return newName;
}
private String exactDirectMatch(final String oldTypeName, final String newTypeName, final String strippedVariableName) {
if (strippedVariableName.equals(oldTypeName))
return newTypeName;
if (strippedVariableName.equals(getLowerCased(oldTypeName)))
return getLowerCased(newTypeName);
return null;
}
private String embeddedMatch(String oldTypeName, String newTypeName, String strippedVariableName) {
// possibility of a match?
final String lowerCaseVariable= strippedVariableName.toLowerCase();
final String lowerCaseOldTypeName= oldTypeName.toLowerCase();
int presumedIndex= lowerCaseVariable.indexOf(lowerCaseOldTypeName);
while (presumedIndex != -1) {
// it may be there
final String presumedTypeName= strippedVariableName.substring(presumedIndex, presumedIndex + oldTypeName.length());
final String prefix= strippedVariableName.substring(0, presumedIndex);
final String suffix= strippedVariableName.substring(presumedIndex + oldTypeName.length());
// can match at all? (depends on suffix)
if (startsNewHunk(suffix)) {
String name= exactMatch(oldTypeName, newTypeName, presumedTypeName);
if (name != null)
return prefix + name + suffix;
}
// did not match -> find next occurrence
presumedIndex= lowerCaseVariable.indexOf(lowerCaseOldTypeName, presumedIndex + 1);
}
return null;
}
private String suffixMatch(final String oldType, final String newType, final String strippedVariableName) {
// get an array of all camel-cased elements from both types + the
// variable
String[] suffixesOld= getSuffixes(oldType);
String[] suffixesNew= getSuffixes(newType);
String[] suffixesVar= getSuffixes(strippedVariableName);
// get an equal-sized array of the last n camel-cased elements
int min= Math.min(suffixesOld.length, suffixesNew.length);
String[] suffixesOldEqual= new String[min];
String[] suffixesNewEqual= new String[min];
System.arraycopy(suffixesOld, suffixesOld.length - min, suffixesOldEqual, 0, min);
System.arraycopy(suffixesNew, suffixesNew.length - min, suffixesNewEqual, 0, min);
// find endIndex. endIndex is the index of the last hunk of the old type
// name in the variable name.
int endIndex= -1;
for (int j= suffixesVar.length - 1; j >= 0; j--) {
String newHunkName= exactMatch(suffixesOldEqual[suffixesOldEqual.length - 1], suffixesNewEqual[suffixesNewEqual.length - 1], suffixesVar[j]);
if (newHunkName != null) {
endIndex= j;
break;
}
}
if (endIndex == -1)
return null; // last hunk not found -> no match
int stepBack= 0;
int lastSuffixMatched= -1;
int hunkInVarName= -1;
for (int i= suffixesOldEqual.length - 1; i >= 0; i--) {
hunkInVarName= endIndex - stepBack;
stepBack++;
if (hunkInVarName < 0) {
// we have reached the beginning of the variable name
break;
}
// try to match this hunk:
String newHunkName= exactMatch(suffixesOldEqual[i], suffixesNewEqual[i], suffixesVar[hunkInVarName]);
if (newHunkName == null)
break; // only match complete suffixes
suffixesVar[hunkInVarName]= newHunkName;
lastSuffixMatched= i;
}
if (lastSuffixMatched == 0) {
// we have matched ALL type hunks in the variable name,
// insert any new prefixes of the new type name
int newPrefixes= suffixesNew.length - suffixesNewEqual.length;
if (newPrefixes > 0) {
// Propagate lowercased start to the front
if (Character.isLowerCase(suffixesVar[hunkInVarName].charAt(0)) && Character.isUpperCase(suffixesOldEqual[lastSuffixMatched].charAt(0))) {
suffixesVar[hunkInVarName]= getUpperCased(suffixesVar[hunkInVarName]);
suffixesNew[0]= getLowerCased(suffixesNew[0]);
}
String[] newVariableName= new String[suffixesVar.length + newPrefixes];
System.arraycopy(suffixesVar, 0, newVariableName, 0, hunkInVarName); // hunks before type name in variable name
System.arraycopy(suffixesNew, 0, newVariableName, hunkInVarName, newPrefixes); // new hunks in new type name
System.arraycopy(suffixesVar, hunkInVarName, newVariableName, hunkInVarName + newPrefixes, suffixesVar.length - hunkInVarName); // matched + rest hunks
suffixesVar= newVariableName;
}
}
String varName= concat(suffixesVar);
if (varName.equals(strippedVariableName))
return null; // no "silly suggestions"
else
return varName;
}
// ---------------- Helper methods
/**
* True if the string is the beginning of a new camel case hunk. False if it
* is not.
*/
private boolean startsNewHunk(String string) {
if (string.length() == 0)
return true;
return isLegalChar(string.charAt(0));
}
/**
* True if hunk is longer than 1 character and all letters in the hunk are
* uppercase. False if not.
*/
private boolean isUpperCaseCamelCaseHunk(String hunk) {
if (hunk.length() < 2)
return false;
for (int i= 0; i < hunk.length(); i++) {
if (!isLegalChar(hunk.charAt(i)))
return false;
}
return true;
}
/**
* False if the character is a letter and it is lowercase. True in all other
* cases.
*/
private boolean isLegalChar(char c) {
if (Character.isLetter(c))
return Character.isUpperCase(c);
return true;
}
/**
* Grab a list of camelCase-separated suffixes from the typeName, for
* example:
*
* "JavaElementName" => { "Java", "Element", "Name }
*
* "ASTNode" => { "AST", "Node" }
*
*/
private String[] getSuffixes(String typeName) {
List suffixes= new ArrayList();
JavaWordIterator iterator= new JavaWordIterator();
iterator.setText(typeName);
int lastmatch= 0;
int match;
while ( (match= iterator.next()) != BreakIterator.DONE) {
suffixes.add(typeName.substring(lastmatch, match));
lastmatch= match;
}
return (String[]) suffixes.toArray(new String[0]);
}
private String concat(String[] suffixesNewEqual) {
StringBuffer returner= new StringBuffer();
for (int j= 0; j < suffixesNewEqual.length; j++) {
returner.append(suffixesNewEqual[j]);
}
return returner.toString();
}
private String getLowerCased(String name) {
if (name.length() > 1)
return Character.toLowerCase(name.charAt(0)) + name.substring(1);
else
return name.toLowerCase();
}
private String getUpperCased(String name) {
if (name.length() > 1)
return Character.toUpperCase(name.charAt(0)) + name.substring(1);
else
return name.toLowerCase();
}
private String getFirstUpperRestLowerCased(String name) {
if (name.length() > 1)
return Character.toUpperCase(name.charAt(0)) + name.substring(1).toLowerCase();
else
return name.toLowerCase();
}
private boolean isInterfaceName(String typeName) {
return ( (typeName.length() >= 2) && typeName.charAt(0) == 'I' && Character.isUpperCase(typeName.charAt(1)));
}
private String getInterfaceName(String typeName) {
return typeName.substring(1);
}
private String findLongestPrefix(String name, String[] prefixes) {
String usedPrefix= ""; //$NON-NLS-1$
int bestLen= 0;
for (int i= 0; i < prefixes.length; i++) {
if (name.startsWith(prefixes[i])) {
if (prefixes[i].length() > bestLen) {
bestLen= prefixes[i].length();
usedPrefix= prefixes[i];
}
}
}
return usedPrefix;
}
private String findLongestSuffix(String name, String[] suffixes) {
String usedPrefix= ""; //$NON-NLS-1$
int bestLen= 0;
for (int i= 0; i < suffixes.length; i++) {
if (name.endsWith(suffixes[i])) {
if (suffixes[i].length() > bestLen) {
bestLen= suffixes[i].length();
usedPrefix= suffixes[i];
}
}
}
return usedPrefix;
}
/**
* Returns true if the type name can be pluralized by a string operation.
* This is always the case if it does not already end with an "s".
*/
private boolean canPluralize(String typeName) {
return !typeName.endsWith(PLURAL_S);
}
private String pluralize(String typeName) {
if (typeName.endsWith(SINGULAR_Y))
typeName= typeName.substring(0, typeName.length() - 1).concat(PLURAL_IES);
else if (!typeName.endsWith(PLURAL_S))
typeName= typeName.concat(PLURAL_S);
return typeName;
}
private void resetPrefixes() {
String[] empty= new String[0];
fFieldPrefixes= empty;
fFieldSuffixes= empty;
fStaticFieldPrefixes= empty;
fStaticFieldSuffixes= empty;
fLocalPrefixes= empty;
fLocalSuffixes= empty;
fArgumentPrefixes= empty;
fArgumentSuffixes= empty;
}
private void initializePrefixesAndSuffixes(IJavaProject project) {
fFieldPrefixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_FIELD_PREFIXES);
fFieldSuffixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_FIELD_SUFFIXES);
fStaticFieldPrefixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_STATIC_FIELD_PREFIXES);
fStaticFieldSuffixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_STATIC_FIELD_SUFFIXES);
fLocalPrefixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_LOCAL_PREFIXES);
fLocalSuffixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_LOCAL_SUFFIXES);
fArgumentPrefixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_ARGUMENT_PREFIXES);
fArgumentSuffixes= readCommaSeparatedPreference(project, JavaCore.CODEASSIST_ARGUMENT_SUFFIXES);
}
private String[] readCommaSeparatedPreference(IJavaProject project, String option) {
String list= project.getOption(option, true);
return list == null ? new String[0] : list.split(","); //$NON-NLS-1$
}
}