/* * 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): * * Portions Copyrighted 2008 Sun Microsystems, Inc. */ package org.netbeans.modules.refactoring.ruby; import java.util.Iterator; import java.util.Set; import javax.swing.text.Document; import org.jrubyparser.ast.ArgumentNode; import org.jrubyparser.ast.ClassNode; import org.jrubyparser.ast.ClassVarAsgnNode; import org.jrubyparser.ast.ClassVarDeclNode; import org.jrubyparser.ast.ClassVarNode; import org.jrubyparser.ast.ConstNode; import org.jrubyparser.ast.GlobalAsgnNode; import org.jrubyparser.ast.GlobalVarNode; import org.jrubyparser.ast.IScopingNode; import org.jrubyparser.ast.InstAsgnNode; import org.jrubyparser.ast.InstVarNode; import org.jrubyparser.ast.MethodDefNode; import org.jrubyparser.ast.ModuleNode; import org.jrubyparser.ast.Node; import org.jrubyparser.ast.SClassNode; import org.jrubyparser.ast.SymbolNode; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.editor.BaseDocument; import org.netbeans.modules.csl.api.ElementKind; import org.netbeans.modules.csl.spi.ParserResult; import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport.Kind; import org.netbeans.modules.ruby.Arity; import org.netbeans.modules.ruby.AstPath; import org.netbeans.modules.ruby.AstUtilities; import org.netbeans.modules.ruby.ContextKnowledge; import org.netbeans.modules.ruby.RubyIndex; import org.netbeans.modules.ruby.RubyType; import org.netbeans.modules.ruby.RubyTypeInferencer; 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.IndexedElement; import org.netbeans.modules.ruby.elements.IndexedMethod; import org.netbeans.modules.ruby.lexer.Call; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.openide.filesystems.FileObject; /** * This is a holder class for a Ruby element as well as its * context - used in various places in the refactoring classes. * These need to be able to be mapped from one AST to another, * and correspond (roughly) to the TreePath, RubyElementCtx, * Element and ElementHandle classes (plus some friends like ParserResult * and FileObject) passed around in the equivalent Java refactoring code. * * @author Tor Norbye */ public class RubyElementCtx { private Node node; private Node root; private ParserResult info; private FileObject fileObject; private AstPath path; private int caret; private BaseDocument document; // TODO - get rid of this, the refactoring code should be completely rewritten to use AST nodes directly private Element element; // Lazily computed private ElementKind kind; private String name; private String simpleName; private Arity arity; private String defClass; public RubyElementCtx(Node root, Node node, Element element, FileObject fileObject, ParserResult info) { initialize(root, node, element, fileObject, info); } /** Create a new element holder representing the node closest to the given caret offset in the given compilation job */ public RubyElementCtx(ParserResult parserResult, int caret) { Node _root = AstUtilities.getRoot(parserResult); int astOffset = AstUtilities.getAstOffset(parserResult, caret); path = new AstPath(_root, astOffset); Node leaf = path.leaf(); if (leaf == null) { return; } Iterator<Node> it = path.leafToRoot(); FindNode: while (it.hasNext()) { leaf = it.next(); switch (leaf.getNodeType()) { case ARGUMENTNODE: case LOCALVARNODE: case LOCALASGNNODE: case DVARNODE: case DASGNNODE: case SYMBOLNODE: case FCALLNODE: case VCALLNODE: case CALLNODE: case GLOBALVARNODE: case GLOBALASGNNODE: case INSTVARNODE: case INSTASGNNODE: case CLASSVARNODE: case CLASSVARASGNNODE: case CLASSVARDECLNODE: case COLON2NODE: case CONSTNODE: case CONSTDECLNODE: break FindNode; } if (!it.hasNext()) { leaf = path.leaf(); break; } } Element _element = AstElement.create(parserResult, leaf); initialize(_root, leaf, _element, RubyUtils.getFileObject(parserResult), parserResult); } /** Create a new element holder representing the given node in the same context as the given existing context */ public RubyElementCtx(RubyElementCtx ctx, Node node) { Element _element = AstElement.create(info, node); initialize(ctx.getRoot(), node, _element, ctx.getFileObject(), ctx.getInfo()); } public RubyElementCtx(IndexedElement element) { ParserResult[] infoHolder = new ParserResult[1]; Node _node = AstUtilities.getForeignNode(element, infoHolder); ParserResult _info = infoHolder[0]; Element e = AstElement.create(_info, _node); FileObject fo = element.getFileObject(); document = RetoucheUtils.getDocument(null, fo); initialize(root, _node, e, fo, _info); } private void initialize(Node root, Node node, Element element, FileObject fileObject, ParserResult info) { this.root = root; this.node = node; this.element = element; this.fileObject = fileObject; this.info = info; } public Node getRoot() { return root; } public Element getElement() { return element; } public void setElement(Element element) { this.element = element; } public FileObject getFileObject() { return fileObject; } public void setFileObject(FileObject fileObject) { this.fileObject = fileObject; } public ElementKind getKind() { if (kind == null) { switch (node.getNodeType()) { case DEFNNODE: case DEFSNODE: kind = AstUtilities.isConstructorMethod((MethodDefNode)node) ? ElementKind.CONSTRUCTOR : ElementKind.METHOD; break; case FCALLNODE: case VCALLNODE: case CALLNODE: kind = ElementKind.METHOD; break; case CLASSNODE: case SCLASSNODE: kind = ElementKind.CLASS; break; case MODULENODE: kind = ElementKind.MODULE; break; case LOCALVARNODE: case LOCALASGNNODE: case DVARNODE: case DASGNNODE: kind = ElementKind.VARIABLE; break; case ARGUMENTNODE: { AstPath _path = getPath(); if (_path.leafParent() instanceof MethodDefNode) { kind = AstUtilities.isConstructorMethod((MethodDefNode)_path.leafParent()) ? ElementKind.CONSTRUCTOR : ElementKind.METHOD; } else { // TODO - are ArgumentNodes used anywhere else? kind = ElementKind.PARAMETER; } break; } case SYMBOLNODE: // Ugh - how do I know what it's referring to - a method? a class? a constant? etc. if (Character.isUpperCase(((SymbolNode)node).getName().charAt(0))) { kind = ElementKind.CLASS; // Or module? Or constants? How do we know? } else { // Or method kind = ElementKind.METHOD; } break; case ALIASNODE: // XXX ugh - how do I know what the alias is referring to? For now just guess METHOD, the most common usage kind = ElementKind.METHOD; break; case COLON2NODE: case CONSTNODE: { Node n = getPath().leafParent(); if (n instanceof ClassNode || n instanceof SClassNode) { kind = ElementKind.CLASS; } else if (n instanceof ModuleNode) { kind = ElementKind.MODULE; } else { if (node instanceof ConstNode) { // It might just be a reference to a class or a module (it probably is!) // so I could look up in the index and see what it is -- a class, a module, or // just some other constant - but that's expensive. For now, optimize for the // common case - a class. kind = ElementKind.CLASS; } else { kind = ElementKind.CONSTANT; } } break; } case CONSTDECLNODE: kind = ElementKind.CONSTANT; break; case GLOBALVARNODE: case GLOBALASGNNODE: kind = ElementKind.GLOBAL; break; case INSTVARNODE: case INSTASGNNODE: case CLASSVARNODE: case CLASSVARASGNNODE: case CLASSVARDECLNODE: kind = ElementKind.FIELD; break; } } return kind; } public void setKind(ElementKind kind) { this.kind = kind; } public Node getNode() { return node; } public void setNode(Node node) { this.node = node; } public ParserResult getInfo() { return info; } public AstPath getPath() { if (path == null) { path = new AstPath(root, node); } return path; } public int getCaret() { return caret; } public String getName() { if (name == null) { String[] names = RetoucheUtils.getNodeNames(node); name = names[0]; simpleName = names[1]; } return name; } public String getSimpleName() { if (name == null) { getName(); } return simpleName; } public void setNames(String name, String simpleName) { this.name = name; this.simpleName = simpleName; } public Arity getArity() { if (arity == null) { if (node instanceof MethodDefNode) { arity = Arity.getDefArity(node); } else if (AstUtilities.isCall(node)) { arity = Arity.getCallArity(node); } else if (node instanceof ArgumentNode) { AstPath _path = getPath(); if (_path.leafParent() instanceof MethodDefNode) { arity = Arity.getDefArity(_path.leafParent()); } } } return arity; } public BaseDocument getDocument() { if (document == null) { document = RetoucheUtils.getDocument(info, RubyUtils.getFileObject(info)); } return document; } private String getViewControllerRequire(FileObject view) { return null; } /** * If the node is a method call, return the class of the method we're * looking for (if any) */ public String getDefClass() { if (defClass == null) { if (RubyUtils.isRhtmlFile(fileObject)) { // TODO - look in the Helper class as well to see if the method is coming from there! // In fact that's probably a more likely home! defClass = "ActionView::Base"; } else if (AstUtilities.isCall(node)) { // Try to figure out the call type from the call BaseDocument doc = getDocument(); TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc); int astOffset = AstUtilities.getCallRange(node).getStart(); Call call = Call.getCallType(doc, th, astOffset); int lexOffset = LexUtilities.getLexerOffset(info, astOffset); RubyType types = RubyType.unknown(); final RubyType callType = call.getType(); if (callType.isKnown() && !call.isLHSConstant()) { types = callType; } String lhs = call.getLhs(); if (!types.isKnown() && lhs != null && node != null && call.isSimpleIdentifier()) { Node method = AstUtilities.findLocalScope(node, getPath()); if (method != null) { // TODO - if the lhs is "foo.bar." I need to split this // up and do it a bit more cleverly ContextKnowledge knowledge = new ContextKnowledge(null, method, node, astOffset, lexOffset, info); RubyTypeInferencer rti = RubyTypeInferencer.create(knowledge, false); types = rti.inferType(lhs); } } else if (call == Call.LOCAL) { // Look in the index to see which method it's coming from... RubyIndex index = RubyIndex.get(info); String fqn = AstUtilities.getFqnName(getPath()); if ((fqn == null) || (fqn.length() == 0)) { fqn = RubyIndex.OBJECT; } Set<IndexedMethod> methods = index.getMethods(getName(), fqn, Kind.EXACT); IndexedMethod method = !methods.isEmpty() ? methods.iterator().next() : index.getSuperMethod(fqn, getName(), false); if (method != null) { defClass = method.getIn(); } // else: It's some unqualified method call we don't recognize - perhaps an attribute? // For now just assume it's a method on this class } if (defClass == null) { // Just an inherited method call? if (!types.isKnown() && (lhs == null || "self".equals(lhs))) { defClass = AstUtilities.getFqnName(getPath()); } else if (types.isKnown()) { // TODO handle all types - types.getRealTypes(); defClass = types.isSingleton() ? types.first() : null; } else { defClass = RubyIndex.UNKNOWN_CLASS; } } } else { if (getPath() != null) { IScopingNode clz = AstUtilities.findClassOrModule(getPath()); if (clz != null) { defClass = AstUtilities.getClassOrModuleName(clz); } } if ((defClass == null) && (element != null)) { defClass = element.getIn(); } if (defClass == null) { defClass = RubyIndex.OBJECT; // NOI18N } } } return defClass; } @Override public String toString() { return "node= " + node + ";kind=" + getKind() + ";" + super.toString(); } /** * Get the prefix of the name which should be "stripped" before letting the user edit the variable, * and put back in when done. For globals for example, it's "$" */ public String getStripPrefix() { if (node instanceof GlobalVarNode || node instanceof GlobalAsgnNode) { return "$"; } else if (node instanceof InstVarNode || node instanceof InstAsgnNode) { return "@"; } else if (node instanceof ClassVarNode || node instanceof ClassVarDeclNode || node instanceof ClassVarAsgnNode) { return "@@"; //} else if (node instanceof SymbolNode) { // Symbols don't include these in their names // return ":"; } // TODO: Blocks - "&" ? // Restargs - "*" ? return null; } }