/******************************************************************************* * Copyright (c) 2000, 2008 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.wst.jsdt.internal.corext.refactoring.rename; import java.util.ArrayList; import java.util.List; import org.eclipse.core.runtime.Assert; import org.eclipse.wst.jsdt.core.IJavaScriptProject; import org.eclipse.wst.jsdt.core.JavaScriptCore; import org.eclipse.wst.jsdt.internal.ui.text.JavaWordIterator; 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 "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. * * * * */ 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 "IJavaScriptElement". 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 * "IJavaScriptElement". 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(IJavaScriptProject 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(IJavaScriptProject 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(IJavaScriptProject project) { fFieldPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_FIELD_PREFIXES); fFieldSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_FIELD_SUFFIXES); fStaticFieldPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_STATIC_FIELD_PREFIXES); fStaticFieldSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_STATIC_FIELD_SUFFIXES); fLocalPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_LOCAL_PREFIXES); fLocalSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_LOCAL_SUFFIXES); fArgumentPrefixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_ARGUMENT_PREFIXES); fArgumentSuffixes= readCommaSeparatedPreference(project, JavaScriptCore.CODEASSIST_ARGUMENT_SUFFIXES); } private String[] readCommaSeparatedPreference(IJavaScriptProject project, String option) { String list= project.getOption(option, true); return list == null ? new String[0] : list.split(","); //$NON-NLS-1$ } }