/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * Contributor(s): * * The Original Software is NetBeans. The Initial Developer of the Original * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun * Microsystems, Inc. All Rights Reserved. * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. */ package org.netbeans.modules.refactoring.ruby.plugins; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.Icon; import javax.swing.text.Document; import org.jrubyparser.ast.AliasNode; import org.jrubyparser.ast.ArgumentNode; import org.jrubyparser.ast.Colon2Node; import org.jrubyparser.ast.DAsgnNode; import org.jrubyparser.ast.DVarNode; import org.jrubyparser.ast.LocalAsgnNode; import org.jrubyparser.ast.LocalVarNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.NodeType; import org.jrubyparser.ast.SymbolNode; import org.jrubyparser.ast.INameNode; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.api.lexer.TokenUtilities; import org.netbeans.api.project.Project; import org.netbeans.modules.csl.api.ElementKind; import org.netbeans.modules.csl.api.Error; import org.netbeans.modules.csl.api.Modifier; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.csl.api.Severity; import org.netbeans.modules.csl.api.UiUtils; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.csl.spi.support.ModificationResult; import org.netbeans.modules.refactoring.api.Problem; import org.netbeans.modules.refactoring.api.ProgressEvent; import org.netbeans.modules.refactoring.api.WhereUsedQuery; import org.netbeans.modules.refactoring.ruby.RetoucheUtils; import org.netbeans.modules.refactoring.ruby.RubyElementCtx; import org.netbeans.modules.refactoring.ruby.WhereUsedElement; import org.netbeans.modules.refactoring.ruby.api.WhereUsedQueryConstants; import org.netbeans.modules.refactoring.spi.RefactoringElementsBag; import org.netbeans.modules.ruby.AstPath; import org.netbeans.modules.ruby.AstUtilities; import org.netbeans.modules.ruby.RubyIndex; import org.netbeans.modules.ruby.RubyUtils; import org.netbeans.modules.ruby.elements.AstElement; import org.netbeans.modules.ruby.elements.Element; import org.netbeans.modules.ruby.elements.IndexedClass; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.netbeans.modules.ruby.rubyproject.RubyBaseProject; import org.openide.filesystems.FileObject; import org.openide.util.NbBundle; /** * Actual implementation of Find Usages query search for Ruby * * @todo Perform index lookups to determine the set of files to be checked! * @todo Scan comments! * @todo Do more prechecks of the elements we're trying to find usages for * * @author Jan Becicka * @author Tor Norbye */ public class RubyWhereUsedQueryPlugin extends RubyRefactoringPlugin { private final WhereUsedQuery refactoring; private final RubyElementCtx searchHandle; private Set<IndexedClass> subclasses; private final String targetName; /** Creates a new instance of WhereUsedQuery */ public RubyWhereUsedQueryPlugin(WhereUsedQuery refactoring) { this.refactoring = refactoring; this.searchHandle = refactoring.getRefactoringSource().lookup(RubyElementCtx.class); targetName = searchHandle.getSimpleName(); } // protected Source getRubySource(Phase p) { // switch (p) { // default: // return RetoucheUtils.getSource(searchHandle.getFileObject()); // } // } @Override public Problem preCheck() { if (!searchHandle.getFileObject().isValid()) { return new Problem(true, NbBundle.getMessage(RubyWhereUsedQueryPlugin.class, "DSC_ElNotAvail")); // NOI18N } return null; } private Set<FileObject> getRelevantFiles(final RubyElementCtx tph) { Set<FileObject> set = new HashSet<FileObject>(); FileObject file = tph.getFileObject(); if (file != null) { if (isFindSubclasses() || isFindDirectSubclassesOnly()) { // No need to do any parsing, we'll be using the index to find these files! set.add(file); String name = tph.getName(); // Find overrides of the class RubyIndex index = RubyIndex.get(RetoucheUtils.getProjectRoots(file)); String fqn = AstUtilities.getFqnName(tph.getPath()); subclasses = index.getSubClasses(null, fqn, name, isFindDirectSubclassesOnly()); return set; } if (tph.getKind() == ElementKind.VARIABLE || tph.getKind() == ElementKind.PARAMETER) { // For local variables, only look in the current file! set.add(file); } else { set.addAll(RetoucheUtils.getRubyFilesInProject(file)); } } return set; // final Element el = tph.resolveElement(info); // if (el.getKind().isField()) { // //get field references from index // set.addAll(idx.getResources(ElementHandle.create((TypeElement)el.getEnclosingElement()), EnumSet.of(ClassIndex.SearchKind.FIELD_REFERENCES), EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } else if (el.getKind().isClass() || el.getKind().isInterface()) { // if (isFindSubclasses()||isFindDirectSubclassesOnly()) { // if (isFindDirectSubclassesOnly()) { // //get direct implementors from index // EnumSet searchKind = EnumSet.of(ClassIndex.SearchKind.IMPLEMENTORS); // set.addAll(idx.getResources(ElementHandle.create((TypeElement)el), searchKind,EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } else { // //itererate implementors recursively // set.addAll(getImplementorsRecursive(idx, cpInfo, (TypeElement)el)); // } // } else { // //get type references from index // set.addAll(idx.getResources(ElementHandle.create((TypeElement) el), EnumSet.of(ClassIndex.SearchKind.TYPE_REFERENCES, ClassIndex.SearchKind.IMPLEMENTORS),EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } // } else if (el.getKind() == ElementKind.METHOD && isFindOverridingMethods()) { // //Find overriding methods // TypeElement type = (TypeElement) el.getEnclosingElement(); // set.addAll(getImplementorsRecursive(idx, cpInfo, type)); // } // if (el.getKind() == ElementKind.METHOD && isFindUsages()) { // //get method references for method and for all it's overriders // Set<ElementHandle<TypeElement>> s = idx.getElements(ElementHandle.create((TypeElement) el.getEnclosingElement()), EnumSet.of(ClassIndex.SearchKind.IMPLEMENTORS),EnumSet.of(ClassIndex.SearchScope.SOURCE)); // for (ElementHandle<TypeElement> eh:s) { // TypeElement te = eh.resolve(info); // if (te==null) { // continue; // } // for (Element e:te.getEnclosedElements()) { // if (e instanceof ExecutableElement) { // if (info.getElements().overrides((ExecutableElement)e, (ExecutableElement)el, te)) { // set.addAll(idx.getResources(ElementHandle.create(te), EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES),EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } // } // } // } // set.addAll(idx.getResources(ElementHandle.create((TypeElement) el.getEnclosingElement()), EnumSet.of(ClassIndex.SearchKind.METHOD_REFERENCES),EnumSet.of(ClassIndex.SearchScope.SOURCE))); //????? // } else if (el.getKind() == ElementKind.CONSTRUCTOR) { // set.addAll(idx.getResources(ElementHandle.create((TypeElement) el.getEnclosingElement()), EnumSet.of(ClassIndex.SearchKind.TYPE_REFERENCES, ClassIndex.SearchKind.IMPLEMENTORS),EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } // } // private Set<FileObject> getImplementorsRecursive(ClassIndex idx, ClasspathInfo cpInfo, TypeElement el) { // Set<FileObject> set = new HashSet<FileObject>(); // LinkedList<ElementHandle<TypeElement>> elements = new LinkedList(idx.getElements(ElementHandle.create(el), // EnumSet.of(ClassIndex.SearchKind.IMPLEMENTORS), // EnumSet.of(ClassIndex.SearchScope.SOURCE))); // HashSet<ElementHandle> result = new HashSet(); // while(!elements.isEmpty()) { // ElementHandle<TypeElement> next = elements.removeFirst(); // result.add(next); // elements.addAll(idx.getElements(next, // EnumSet.of(ClassIndex.SearchKind.IMPLEMENTORS), // EnumSet.of(ClassIndex.SearchScope.SOURCE))); // } // for (ElementHandle<TypeElement> e : result) { // FileObject fo = SourceUtils.getFile(e, cpInfo); // assert fo != null: "issue 90196, Cannot find file for " + e + ". cpInfo=" + cpInfo ; // set.add(fo); // } // return set; // } //@Override public Problem prepare(final RefactoringElementsBag elements) { Set<FileObject> a = getRelevantFiles(searchHandle); fireProgressListenerStart(ProgressEvent.START, a.size()); processFiles(a, new FindTask(elements)); fireProgressListenerStop(); return null; } public Problem fastCheckParameters() { if (targetName == null) { return new Problem(true, "Cannot determine target name. Please file a bug with detailed information on how to reproduce (preferably including the current source file and the cursor position)"); } if (searchHandle.getKind() == ElementKind.METHOD) { return checkParametersForMethod(isFindOverridingMethods(), isFindUsages()); } return null; } public Problem checkParameters() { return null; } private Problem checkParametersForMethod(boolean overriders, boolean usages) { if (!(usages || overriders)) { return new Problem(true, NbBundle.getMessage(RubyWhereUsedQueryPlugin.class, "MSG_NothingToFind")); } else return null; } private boolean isFindSubclasses() { return refactoring.getBooleanValue(WhereUsedQueryConstants.FIND_SUBCLASSES); } private boolean isFindUsages() { return refactoring.getBooleanValue(WhereUsedQuery.FIND_REFERENCES); } private boolean isFindDirectSubclassesOnly() { return refactoring.getBooleanValue(WhereUsedQueryConstants.FIND_DIRECT_SUBCLASSES); } private boolean isFindOverridingMethods() { return refactoring.getBooleanValue(WhereUsedQueryConstants.FIND_OVERRIDING_METHODS); } private boolean isSearchFromBaseClass() { return false; } private boolean isSearchInComments() { return refactoring.getBooleanValue(WhereUsedQuery.SEARCH_IN_COMMENTS); } private class FindTask extends TransformTask { private RefactoringElementsBag elements; public FindTask(RefactoringElementsBag elements) { super(); this.elements = elements; } protected Collection<ModificationResult> process(ParserResult parserResult) { if (isCancelled()) { return Collections.<ModificationResult>emptySet(); } Error error = null; RubyElementCtx searchCtx = searchHandle; Node root = AstUtilities.getRoot(parserResult); if (root == null) { //System.out.println("Skipping file " + workingCopy.getFileObject()); // See if the document contains references to this symbol and if so, put a warning in String sourceText = parserResult.getSnapshot().getText().toString(); if (sourceText != null && sourceText.indexOf(targetName) != -1) { int start = 0; int end = 0; String desc = "Parse error in file which contains " + targetName + " reference - skipping it"; List<? extends Error> errors = parserResult.getDiagnostics(); if (errors.size() > 0) { for (Error e : errors) { if (e.getSeverity() == Severity.ERROR) { error = e; break; } } if (error == null) { error = errors.get(0); } String errorMsg = error.getDisplayName(); if (errorMsg.length() > 80) { errorMsg = errorMsg.substring(0, 77) + "..."; // NOI18N } desc = desc + "; " + errorMsg; start = error.getStartPosition(); start = LexUtilities.getLexerOffset(parserResult, start); if (start == -1) { start = 0; } end = start; } Set<Modifier> modifiers = Collections.emptySet(); Icon icon = UiUtils.getElementIcon(ElementKind.ERROR, modifiers); OffsetRange range = new OffsetRange(start, end); WhereUsedElement element = WhereUsedElement.create(parserResult, targetName, desc, range, icon); elements.add(refactoring, element); } } if (error == null && isSearchInComments()) { Document doc = RetoucheUtils.getDocument(parserResult, RubyUtils.getFileObject(parserResult)); if (doc != null) { //force open TokenHierarchy<Document> th = TokenHierarchy.get(doc); TokenSequence<?> ts = th.tokenSequence(); ts.move(0); searchTokenSequence(parserResult, ts); } } if (root == null) { // TODO - warn that this file isn't compileable and is skipped? return Collections.<ModificationResult>emptySet(); } Element element = AstElement.create(parserResult, root); Node node = searchCtx.getNode(); RubyElementCtx fileCtx = new RubyElementCtx(root, node, element, RubyUtils.getFileObject(parserResult), parserResult); // If it's a local search, use a simpler search routine // TODO: ArgumentNode - look to see if we're in a parameter list, and if so its a localvar // (if not, it's a method) if (isFindSubclasses() || isFindDirectSubclassesOnly()) { // I'm only looking for the specific classes assert subclasses != null; // Look in these files for the given classes //findSubClass(root); for (IndexedClass clz : subclasses) { RubyElementCtx matchCtx = new RubyElementCtx(clz); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } } else if (isFindUsages()) { Node method = null; if (node instanceof ArgumentNode) { AstPath path = searchCtx.getPath(); assert path.leaf() == node; Node parent = path.leafParent(); if (!(parent instanceof MethodDefNode)) { method = AstUtilities.findLocalScope(node, path); } } else if (node instanceof LocalVarNode || node instanceof LocalAsgnNode || node instanceof DAsgnNode || node instanceof DVarNode) { // A local variable read or a parameter read, or an assignment to one of these AstPath path = searchCtx.getPath(); method = AstUtilities.findLocalScope(node, path); } if (method != null) { findLocal(searchCtx, fileCtx, method, targetName); } else { // Full AST search // TODO: If it's a local variable, parameter or dynamic variable, limit search to the current scope! AstPath path = new AstPath(); path.descend(root); find(path, searchCtx, fileCtx, root, targetName, Character.isUpperCase(targetName.charAt(0))); path.ascend(); } // TODO: Comment search // TODO: ClassSearch: If looking for subtypes only, do something special here... // (in fact, I should be able to ONLY use the index, correct?) // TODO } else if (isFindOverridingMethods()) { // TODO } else if (isSearchFromBaseClass()) { // TODO } return Collections.<ModificationResult>emptySet(); } private void searchTokenSequence(ParserResult info, TokenSequence<?> ts) { if (ts.moveNext()) { do { Token<?> token = ts.token(); TokenId id = token.id(); String primaryCategory = id.primaryCategory(); if ("comment".equals(primaryCategory) || "block-comment".equals(primaryCategory)) { // NOI18N // search this comment assert targetName != null; CharSequence tokenText = token.text(); if (tokenText == null || targetName == null) { continue; } int index = TokenUtilities.indexOf(tokenText, targetName); if (index != -1) { String text = tokenText.toString(); // TODO make sure it's its own word. Technically I could // look at identifier chars like "_" here but since they are // used for other purposes in comments, consider letters // and numbers as enough if ((index == 0 || !Character.isLetterOrDigit(text.charAt(index-1))) && (index+targetName.length() >= text.length() || !Character.isLetterOrDigit(text.charAt(index+targetName.length())))) { int start = ts.offset() + index; int end = start + targetName.length(); // TODO - get a comment-reference icon? For now, just use the icon type // of the search target Set<Modifier> modifiers = Collections.emptySet(); if (searchHandle.getElement() != null) { modifiers = searchHandle.getElement().getModifiers(); } Icon icon = UiUtils.getElementIcon(searchHandle.getKind(), modifiers); OffsetRange range = new OffsetRange(start, end); WhereUsedElement element = WhereUsedElement.create(info, targetName, range, icon); elements.add(refactoring, element); } } } else { TokenSequence<?> embedded = ts.embedded(); if (embedded != null) { searchTokenSequence(info, embedded); } } } while (ts.moveNext()); } } /** * @todo P1: This is matching method names on classes that have nothing to do with the class we're searching for * - I've gotta filter fields, methods etc. that are not in the current class * (but I also have to search for methods that are OVERRIDING the class... so I've gotta work a little harder!) * @todo Arity matching on the methods to preclude methods that aren't overriding or aliasing! */ @SuppressWarnings("fallthrough") private void find(AstPath path, RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name, boolean upperCase) { /*if (node instanceof ArgumentNode) { if (((ArgumentNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } } else*/ if (node.getNodeType() == NodeType.ALIASNODE) { AliasNode an = (AliasNode)node; if (an.getNewName().equals(name) || an.getOldName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } } else if (!upperCase) { // Local variables - I can be smarter about context searches here! // Methods, attributes, etc. // TODO - be more discriminating on the filetype switch (node.getNodeType()) { case DEFNNODE: case DEFSNODE: if (((MethodDefNode)node).getName().equals(name)) { boolean skip = false; // Check that we're in a class or module we're interested in String fqn = AstUtilities.getFqnName(path); if (fqn == null || fqn.length() == 0) { fqn = RubyIndex.OBJECT; } if (!fqn.equals(searchCtx.getDefClass())) { // XXX THE ABOVE IS NOT RIGHT - I shouldn't // use equals on the class names, I should use the // index and see if one derives from includes the other // skip = true; } // Check arity if (!skip && AstUtilities.isCall(searchCtx.getNode())) { // The reference is a call and this is a definition; see if // this looks like a match // TODO - enforce that this method is also in the desired // target class!!! if (!AstUtilities.isCallFor(searchCtx.getNode(), searchCtx.getArity(), node)) { skip = true; } } else { // The search handle is a method def, as is this, with the same name. // Now I need to go and see if this is an override (e.g. compatible // arglist...) // XXX TODO } if (!skip) { node = AstUtilities.getDefNameNode((MethodDefNode)node); // Found a method match // TODO - check arity - see OccurrencesFinder RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } } break; case FCALLNODE: if (AstUtilities.isAttr(node)) { SymbolNode[] symbols = AstUtilities.getAttrSymbols(node); for (SymbolNode symbol : symbols) { if (symbol.getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } } } // Fall through for other call checking case VCALLNODE: case CALLNODE: if (((INameNode)node).getName().equals(name)) { // TODO - if it's a call without a lhs (e.g. Call.LOCAL), // make sure that we're referring to the same method call // Found a method call match // TODO - make a node on the same line // TODO - check arity - see OccurrencesFinder RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; case SYMBOLNODE: if (((SymbolNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; case GLOBALVARNODE: case GLOBALASGNNODE: case INSTVARNODE: case INSTASGNNODE: case CLASSVARNODE: case CLASSVARASGNNODE: case CLASSVARDECLNODE: if (((INameNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; } } else { // Classes, modules, constants, etc. switch (node.getNodeType()) { case COLON2NODE: { Colon2Node c2n = (Colon2Node)node; if (c2n.getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; } case CONSTNODE: case CONSTDECLNODE: if (((INameNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; } } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } path.descend(child); find(path, searchCtx, fileCtx, child, name, upperCase); path.ascend(); } } /** Search for local variables in local scope */ private void findLocal(RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name) { switch (node.getNodeType()) { case ARGUMENTNODE: // TODO - check parent and make sure it's not a method of the same name? // e.g. if I have "def foo(foo)" and I'm searching for "foo" (the parameter), // I don't want to pick up the ArgumentNode under def foo that corresponds to the // "foo" method name! if (((ArgumentNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; // I don't have alias nodes within a method, do I? // } else if (node instanceof AliasNode) { // AliasNode an = (AliasNode)node; // if (an.getNewName().equals(name) || an.getOldName().equals(name)) { // RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); // elements.add(refactoring, WhereUsedElement.create(matchCtx)); // } // break; case LOCALVARNODE: case LOCALASGNNODE: if (((INameNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; case DVARNODE: case DASGNNODE: if (((INameNode)node).getName().equals(name)) { // Found a method call match // TODO - make a node on the same line // TODO - check arity - see OccurrencesFinder RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; case SYMBOLNODE: // XXX Can I have symbols to local variables? Try it!!! if (((SymbolNode)node).getName().equals(name)) { RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node); elements.add(refactoring, WhereUsedElement.create(matchCtx)); } break; } List<Node> list = node.childNodes(); for (Node child : list) { if (child.isInvisible()) { continue; } findLocal(searchCtx, fileCtx, child, name); } } // private void findSubClass(Node node) { // @SuppressWarnings("unchecked") // List<Node> list = node.childNodes(); // // for (Node child : list) { // findSubClass(child); // } // } } }