| /******************************************************************************* |
| * Copyright (c) 2000, 2007 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 |
| * |
| |
| *******************************************************************************/ |
| package org.eclipse.dltk.internal.corext.refactoring.rename; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.dltk.core.DLTKCore; |
| import org.eclipse.dltk.core.IScriptProject; |
| import org.eclipse.dltk.internal.ui.text.DLTKWordIterator; |
| |
| import com.ibm.icu.text.BreakIterator; |
| |
| /** |
| * 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 "ScriptElement()". |
| * |
| * 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 "createmodelElement()" or |
| * fields like "fmodelElementCache". |
| * |
| * 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, |
| * "SimpleScriptElement" is split into the three hunks "Simple", |
| * "Script", 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. |
| * |
| * |
| * |
| * |
| */ |
| 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 "IScriptElement". 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 "modelElement" for type |
| * "IScriptElement". 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(IScriptProject 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(IScriptProject 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: |
| * |
| * "ScriptElementName" => { "Script", "Element", "Name } |
| * |
| * "ASTNode" => { "AST", "Node" } |
| * |
| */ |
| private String[] getSuffixes(String typeName) { |
| List suffixes= new ArrayList(); |
| DLTKWordIterator iterator= new DLTKWordIterator(); |
| 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(IScriptProject project) { |
| if (DLTKCore.DEBUG) { |
| System.err.println("Add code assist code here..."); //$NON-NLS-1$ |
| } |
| // fFieldPrefixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_FIELD_PREFIXES); |
| // fFieldSuffixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_FIELD_SUFFIXES); |
| // fStaticFieldPrefixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_STATIC_FIELD_PREFIXES); |
| // fStaticFieldSuffixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_STATIC_FIELD_SUFFIXES); |
| // fLocalPrefixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_LOCAL_PREFIXES); |
| // fLocalSuffixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_LOCAL_SUFFIXES); |
| // fArgumentPrefixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_ARGUMENT_PREFIXES); |
| // fArgumentSuffixes= readCommaSeparatedPreference(project, DLTKCore.CODEASSIST_ARGUMENT_SUFFIXES); |
| } |
| |
| // private String[] readCommaSeparatedPreference(IScriptProject project, String option) { |
| // String list= project.getOption(option, true); |
| // return list == null ? new String[0] : list.split(","); //$NON-NLS-1$ |
| // } |
| |
| } |