package com.aptana.rdt.internal.parser.warnings; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.jruby.ast.ArgsNode; import org.jruby.ast.BlockNode; import org.jruby.ast.ClassNode; import org.jruby.ast.ClassVarAsgnNode; import org.jruby.ast.ClassVarDeclNode; import org.jruby.ast.ClassVarNode; import org.jruby.ast.DefnNode; import org.jruby.ast.DefsNode; import org.jruby.ast.InstAsgnNode; import org.jruby.ast.InstVarNode; import org.jruby.ast.ListNode; import org.jruby.ast.LocalAsgnNode; import org.jruby.ast.LocalVarNode; import org.jruby.ast.MethodDefNode; import org.jruby.ast.Node; import org.jruby.ast.RootNode; import org.jruby.ast.SClassNode; import org.jruby.ast.VCallNode; import org.jruby.ast.types.INameNode; import org.rubypeople.rdt.core.parser.warnings.RubyLintVisitor; import com.aptana.rdt.AptanaRDTPlugin; public class SimilarVariableNameVisitor extends RubyLintVisitor { private Map<Node, Map<String, Node>> scopesToVars; private List<Node> scopes; public SimilarVariableNameVisitor(String contents) { super(AptanaRDTPlugin.getDefault().getOptions(), contents); scopes = new ArrayList<Node>(); scopesToVars = new HashMap<Node, Map<String,Node>>(); } protected String getOptionKey() { return AptanaRDTPlugin.COMPILER_PB_SIMILAR_VARIABLE_NAMES; } public Object visitDefnNode(DefnNode iVisited) { enterMethod(iVisited); return super.visitDefnNode(iVisited); } public Object visitDefsNode(DefsNode iVisited) { enterMethod(iVisited); return super.visitDefsNode(iVisited); } private void enterMethod(MethodDefNode node) { enterScope(node); } public Object visitClassNode(ClassNode visited) { enterScope(visited); return super.visitClassNode(visited); } public Object visitArgsNode(ArgsNode iVisited) { ListNode list = iVisited.getPre(); if (list != null && list.childNodes() != null) { for (Object arg : list.childNodes()) { Node argNode = (Node) arg; addVar((INameNode) argNode); } } list = iVisited.getOptArgs(); if (list != null && list.childNodes() != null) { for (Object arg : list.childNodes()) { Node argNode = (Node) arg; addVar((INameNode) argNode); } } return super.visitArgsNode(iVisited); } public void exitDefnNode(DefnNode iVisited) { exitMethod(); super.exitDefnNode(iVisited); } public Object visitBlockNode(BlockNode iVisited) { enterScope(iVisited); return super.visitBlockNode(iVisited); } private void enterScope(Node node) { scopes.add(node); scopesToVars.put(node, new HashMap<String, Node>()); } public void exitBlockNode(BlockNode iVisited) { exitScope(); super.exitBlockNode(iVisited); } private void exitClass() { exitScope(true); } private void exitScope() { exitScope(false); } private void exitScope(boolean exitingClass) { Map<String, Node> vars = getAllVarsInScope(); pop(); // FIXME Only create warnings on references to variables that have no declaration List<String> names = new ArrayList<String>(vars.keySet()); while (!names.isEmpty()) { String name = names.remove(0); boolean isInstanceVar = isInstanceVar(name); boolean isClassVar = isClassVar(name); if (!exitingClass && (isClassVar || isInstanceVar)) { continue; } for (String string : names) { // Strip off @s? String modName = name; if (isInstanceVar) { if (!isInstanceVar(string)) continue; modName = name.substring(1); string = string.substring(1); } else if (isClassVar) { if (!isClassVar(string)) continue; modName = name.substring(2); string = string.substring(2); } else { // name is local var if (isInstanceVar(string) || isClassVar(string)) continue; } if (isPlural(modName, string) || isPlural(string, modName)) { // check for one being plural of other, if so skip them // FIXME Make this option configurable! continue; } if (damerauLevenshteinDistance(modName, string) <= levenshteinThreshold(modName)) { createProblem(vars.get(name).getPosition(), "Variable has similar name to another in scope: Possible misspelling."); // FIXME Shouldn't create a problem if there is a method with this name! } } } } private Map<String, Node> getAllVarsInScope() { Map<String, Node> all = new HashMap<String, Node>(); for (Node scopeNode : scopes) { all.putAll(scopesToVars.get(scopeNode)); } return all; } private void pop() { Node scopeNode = scopes.remove(scopes.size() - 1); scopesToVars.remove(scopeNode); } private boolean isPlural(String singular, String plural) { return (singular.length() == plural.length() - 1) && (singular.equals(plural.substring(0, plural.length() - 1))) && plural.charAt(plural.length() - 1) == 's'; } private boolean isInstanceVar(String name) { return !isClassVar(name) && name.startsWith("@"); } private boolean isClassVar(String name) { return name.startsWith("@@"); } /** * Determine how many changes should trigger the problem */ private int levenshteinThreshold(String name) { int length = name.length(); if (length < 3) return 0; return (int) Math.ceil(length / 5.0f); } /** * This is an implementation created from translating the pseudocode from * Wikipedia: http://en.wikipedia.org/wiki/Damerau-Levenshtein_distance * * @param s * @param t * @return */ private int damerauLevenshteinDistance(String s, String t) { if (s == null || t == null) { throw new IllegalArgumentException("Strings must not be null"); } int m = s.length(); int n = t.length(); if (n == 0) { return m; } else if (m == 0) { return n; } // d is a table with m+1 rows and n+1 columns int[][] d = new int[m + 1][n + 1]; for (int i = 0; i <= m; i++) d[i][0] = i; for (int j = 1; j <= n; j++) d[0][j] = j; for (int i = 1; i <= m; i++) { for (int j = 1; j <= n; j++) { int cost; if (s.charAt(i - 1) == t.charAt(j - 1)) { cost = 0; } else { cost = 1; } d[i][j] = minimum(d[i - 1][j] + 1, // deletion d[i][j - 1] + 1, // insertion d[i - 1][j - 1] + cost // substitution ); if (i > 1 && j > 1 && s.charAt(i - 1) == t.charAt(j - 2) && s.charAt(i - 2) == t.charAt(j - 1)) { d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost // transposition ); } } } return d[m][n]; } private int minimum(int i, int j, int k) { return Math.min(Math.min(i, j), k); } public Object visitVCallNode(VCallNode iVisited) { addVar(iVisited); return super.visitVCallNode(iVisited); } public Object visitLocalAsgnNode(LocalAsgnNode iVisited) { addVar(iVisited); return super.visitLocalAsgnNode(iVisited); } public Object visitLocalVarNode(LocalVarNode iVisited) { addVar(iVisited); return super.visitLocalVarNode(iVisited); } private void addToClass(INameNode iVisited) { int i = 1; Node scopeNode = null; while( true ) { scopeNode = scopes.get(scopes.size() - i); if (scopeNode instanceof ClassNode || scopeNode instanceof SClassNode || scopeNode instanceof RootNode) { addToScope(iVisited, scopeNode); break; } i++; if (i > scopes.size()) break; } } private void addVar(INameNode iVisited) { Node scopeNode = scopes.get(scopes.size() - 1); addToScope(iVisited, scopeNode); } private void addToScope(INameNode iVisited, Node scopeNode) { Map<String, Node> vars = scopesToVars.get(scopeNode); if (vars == null) { vars = new HashMap<String, Node>(); } vars.put(iVisited.getName(), (Node) iVisited); } public Object visitRootNode(RootNode visited) { enterScope(visited); return super.visitRootNode(visited); } public void exitClassNode(ClassNode iVisited) { exitClass(); } public void exitDefsNode(DefsNode iVisited) { exitMethod(); super.exitDefsNode(iVisited); } private void exitMethod() { // TODO Check for references to local variables that have no declaration/assignment exitScope(); } public Object visitInstAsgnNode(InstAsgnNode iVisited) { addToClass(iVisited); return super.visitInstAsgnNode(iVisited); } public Object visitInstVarNode(InstVarNode iVisited) { addToClass(iVisited); return super.visitInstVarNode(iVisited); } public Object visitClassVarAsgnNode(ClassVarAsgnNode iVisited) { addToClass(iVisited); return super.visitClassVarAsgnNode(iVisited); } public Object visitClassVarNode(ClassVarNode iVisited) { addToClass(iVisited); return super.visitClassVarNode(iVisited); } public Object visitClassVarDeclNode(ClassVarDeclNode iVisited) { addToClass(iVisited); return super.visitClassVarDeclNode(iVisited); } }