ruby/src/org/netbeans/modules/ruby/RubyRenameHandler.java
author enebo@netbeans.org
Sun, 08 Dec 2013 11:39:16 -0600
changeset 4554 07958c1ff402
parent 4549 a8ced3d20fca
child 4555 3773928e70d0
permissions -rw-r--r--
Too much stuff in one commit. Rename blocks now follows language semantics. Removal of more ASTPath consumers
     1 /*
     2  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
     3  *
     4  * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
     5  *
     6  * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
     7  * Other names may be trademarks of their respective owners.
     8  *
     9  * The contents of this file are subject to the terms of either the GNU
    10  * General Public License Version 2 only ("GPL") or the Common
    11  * Development and Distribution License("CDDL") (collectively, the
    12  * "License"). You may not use this file except in compliance with the
    13  * License. You can obtain a copy of the License at
    14  * http://www.netbeans.org/cddl-gplv2.html
    15  * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
    16  * specific language governing permissions and limitations under the
    17  * License.  When distributing the software, include this License Header
    18  * Notice in each file and include the License file at
    19  * nbbuild/licenses/CDDL-GPL-2-CP.  Oracle designates this
    20  * particular file as subject to the "Classpath" exception as provided
    21  * by Oracle in the GPL Version 2 section of the License file that
    22  * accompanied this code. If applicable, add the following below the
    23  * License Header, with the fields enclosed by brackets [] replaced by
    24  * your own identifying information:
    25  * "Portions Copyrighted [year] [name of copyright owner]"
    26  *
    27  * Contributor(s):
    28  *
    29  * The Original Software is NetBeans. The Initial Developer of the Original
    30  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
    31  * Microsystems, Inc. All Rights Reserved.
    32  *
    33  * If you wish your version of this file to be governed by only the CDDL
    34  * or only the GPL Version 2, indicate your decision by adding
    35  * "[Contributor] elects to include this software in this distribution
    36  * under the [CDDL or GPL Version 2] license." If you do not indicate a
    37  * single choice of license, a recipient has the option to distribute
    38  * your version of this file under either the CDDL, the GPL Version 2 or
    39  * to extend the choice of license to its licensees as provided above.
    40  * However, if you add GPL Version 2 code and therefore, elected the GPL
    41  * Version 2 license, then the option applies only if the new code is
    42  * made subject to such option by the copyright holder.
    43  */
    44 package org.netbeans.modules.ruby;
    45 
    46 import java.util.Collections;
    47 import java.util.HashSet;
    48 import java.util.Set;
    49 
    50 import org.jrubyparser.ast.ArgsNode;
    51 import org.jrubyparser.ast.ArgumentNode;
    52 import org.jrubyparser.ast.BlockArgNode;
    53 import org.jrubyparser.ast.ListNode;
    54 import org.jrubyparser.ast.LocalAsgnNode;
    55 import org.jrubyparser.ast.LocalVarNode;
    56 import org.jrubyparser.ast.MethodDefNode;
    57 import org.jrubyparser.ast.Node;
    58 import org.jrubyparser.ast.NodeType;
    59 import org.jrubyparser.ast.INameNode;
    60 import org.jrubyparser.SourcePosition;
    61 import org.jrubyparser.ast.AssignableNode;
    62 import org.jrubyparser.ast.ILocalScope;
    63 import org.jrubyparser.ast.ILocalVariable;
    64 import org.netbeans.modules.csl.api.InstantRenamer;
    65 import org.netbeans.modules.csl.api.OffsetRange;
    66 import org.netbeans.modules.csl.spi.ParserResult;
    67 import org.netbeans.modules.ruby.lexer.LexUtilities;
    68 import org.openide.util.NbBundle;
    69 
    70 /**
    71  * Handle renaming of local elements
    72  * @todo I should be able to rename top-level methods as well since they
    73  *   are private
    74  * @todo Rename |j| in the following will only rename "j" inside the block!
    75  * <pre>
    76 i = 50
    77 j = 200
    78 k = 100
    79 x = [1,2,3]
    80 x.each do |j|
    81   puts j
    82 end
    83 puts j
    84  * </pre>
    85  * @todo When you fix, make sure BlockarReuse is also fixed!
    86  * @todo Try renaming "hello" in the exception here; my code is confused
    87  *   about what I'm renaming (aliases method name) and the refactoring dialog
    88  *   name is wrong! This is happening because it's also changing GlobalAsgnNode for $!
    89  *   but its parent is LocalAsgnNode, and -its- -grand- parent is a RescueBodyNode! 
    90  *   I should special case this!
    91  * <pre>
    92 def hello
    93   begin
    94     ex = 50
    95     puts "test"
    96   
    97   rescue Exception => hello
    98     puts hello
    99   end
   100 end
   101  *
   102  * </pre>
   103  *
   104  * @author Tor Norbye
   105  */
   106 public class RubyRenameHandler implements InstantRenamer {
   107     
   108     public RubyRenameHandler() {
   109     }
   110 
   111     @Override
   112     public boolean isRenameAllowed(ParserResult info, int caretOffset, String[] explanationRetValue) {
   113         Node root = AstUtilities.getRoot(info);
   114 
   115         if (root == null) {
   116             explanationRetValue[0] = NbBundle.getMessage(RubyRenameHandler.class, "NoRenameWithErrors");
   117             return false;
   118         }
   119 
   120         Node closest = root.getNodeAt(AstUtilities.getAstOffset(info, caretOffset));
   121         if (closest == null) return false;
   122         if (closest instanceof ILocalVariable) return true;  // All local block/method vars can be renamed
   123 
   124         switch (closest.getNodeType()) {
   125         case INSTASGNNODE: case INSTVARNODE: case CLASSVARDECLNODE: case CLASSVARNODE: case CLASSVARASGNNODE:
   126         case GLOBALASGNNODE: case GLOBALVARNODE: case CONSTDECLNODE: case CONSTNODE: case DEFNNODE: case DEFSNODE:
   127         case FCALLNODE: case CALLNODE: case VCALLNODE: case COLON2NODE: case COLON3NODE: case ALIASNODE:
   128         case SYMBOLNODE: // TODO - what about the string arguments in an alias node? Gotta check those
   129             return true;
   130         }
   131 
   132         return false;
   133     }
   134 
   135     @Override
   136     public Set<OffsetRange> getRenameRegions(ParserResult info, int caretOffset) {
   137         Node root = AstUtilities.getRoot(info);
   138         if (root == null) return Collections.emptySet();
   139 
   140         Set<OffsetRange> regions = new HashSet<OffsetRange>();
   141 
   142         int astOffset = AstUtilities.getAstOffset(info, caretOffset);
   143         if (astOffset == -1) return Collections.emptySet();
   144 
   145         AstPath path = new AstPath(root, astOffset);
   146         Node closest = path.leaf();
   147         if (closest == null) return Collections.emptySet();
   148 
   149         if (closest.isBlockParameter()) {
   150             ILocalVariable blockParameter = (ILocalVariable) closest;
   151             
   152             for (ILocalVariable occurrence: blockParameter.getOccurrences()) {
   153                 OffsetRange range = LexUtilities.getLexerOffsets(info, 
   154                         AstUtilities.offsetRangeFor(occurrence.getLexicalNamePosition()));
   155                 
   156                 if (range != OffsetRange.NONE) regions.add(range);
   157             }         
   158         } else if (closest instanceof LocalVarNode || closest instanceof LocalAsgnNode) {
   159             // A local variable read or a parameter read, or an assignment to one of these
   160             String name = ((INameNode)closest).getName();
   161             Node localScope = AstUtilities.findLocalScope(closest, path);
   162 
   163             if (localScope == null) {
   164                 // Use parent, possibly Grand Parent if we have a newline node in the way
   165                 localScope = path.leafParent();
   166 
   167                 if (localScope.getNodeType() == NodeType.NEWLINENODE) localScope = path.leafGrandParent();
   168                 if (localScope == null) localScope = closest;
   169             }
   170 
   171             addLocals(info, localScope, name, regions);
   172         } else if (closest.getNodeType() == NodeType.DVARNODE || closest.getNodeType() == NodeType.DASGNNODE) {
   173             // 1.8-style block declaration: iter { dasgn }
   174             String name = ((INameNode)closest).getName();
   175             
   176             for (Node block : AstUtilities.getApplicableBlocks(path, true)) {
   177                 addDynamicVars(info, block, name, regions);
   178             }
   179         } else if (closest.getNodeType() == NodeType.ARGUMENTNODE || closest.getNodeType() == NodeType.BLOCKARGNODE) {
   180             // A method name (if under a DefnNode or DefsNode) or a parameter (if indirectly under an ArgsNode)
   181             String name = ((INameNode)closest).getName();
   182 
   183             Node parent = path.leafParent();
   184 
   185             if (parent != null) {
   186                 // Make sure it's a parameter, not a method
   187                 if (!(parent instanceof MethodDefNode)) {
   188                     // Parameter (check to see if its under ArgumentNode)
   189                     Node method = AstUtilities.findMethodAtOffset(root, astOffset);
   190 
   191                     if (method == null) method = AstUtilities.findBlock(path);
   192 
   193                     if (method == null) {
   194                         // Use parent, possibly Grand Parent if we have a newline node in the way
   195                         method = path.leafParent();
   196 
   197                         if (method.getNodeType() == NodeType.NEWLINENODE) method = path.leafGrandParent();
   198                         if (method == null) method = closest;
   199                     }
   200 
   201                     addLocals(info, method, name, regions);
   202                 }
   203             }
   204         }
   205 
   206         return regions;
   207     }
   208 
   209     private void addLocals(ParserResult info, Node node, String name, Set<OffsetRange> ranges) {
   210         if (node.getNodeType() == NodeType.LOCALVARNODE) {
   211             if (((INameNode)node).getName().equals(name)) {
   212                 OffsetRange range = AstUtilities.getRange(node);
   213                 range = LexUtilities.getLexerOffsets(info, range);
   214                 if (range != OffsetRange.NONE) ranges.add(range);
   215             }
   216         } else if (node.getNodeType() == NodeType.LOCALASGNNODE) {
   217             if (((INameNode)node).getName().equals(name)) {
   218                 OffsetRange range = AstUtilities.getRange(node);
   219                 // Adjust end offset to only include the left hand size
   220                 range = new OffsetRange(range.getStart(), range.getStart() + name.length());
   221                 range = LexUtilities.getLexerOffsets(info, range);
   222                 if (range != OffsetRange.NONE) ranges.add(range);
   223             }
   224         } else if (node.getNodeType() == NodeType.ARGSNODE) {
   225             addArgsNode(node, name, info, ranges);
   226         }
   227 
   228         for (Node child : node.childNodes()) {
   229             addLocals(info, child, name, ranges);
   230         }
   231     }
   232 
   233     // TODO: Check
   234     //  quick tip renaming
   235     //  unused detection
   236     //  occurrences marking
   237     //  code completion
   238     //  live code templates
   239     // ...anyone else who calls findBlock
   240     //
   241     // Test both parent blocks, sibling blocks and descendant blocks
   242     // Make sure the "isUsed" detection is smarter too.
   243     
   244     private void addDynamicVars(ParserResult info, Node node, String name, Set<OffsetRange> ranges) {
   245         switch (node.getNodeType()) {
   246         case DVARNODE:
   247             if (((INameNode)node).getName().equals(name)) {
   248                 OffsetRange range = LexUtilities.getLexerOffsets(info, AstUtilities.getRange(node));
   249                 if (range != OffsetRange.NONE) ranges.add(range);
   250             }
   251             break;
   252         case DASGNNODE:
   253             if (((INameNode)node).getName().equals(name)) {
   254                 OffsetRange range = AstUtilities.offsetRangeFor(((AssignableNode) node).getLeftHandSidePosition());
   255                 range = LexUtilities.getLexerOffsets(info, range);
   256                 if (range != OffsetRange.NONE) ranges.add(range);
   257             }
   258             break;
   259         case ARGSNODE:
   260             addArgsNode(node, name, info, ranges);
   261         }
   262 
   263         for (Node child : node.childNodes()) {
   264             if (child instanceof ILocalScope || child.getNodeType() == NodeType.ITERNODE) continue;
   265 
   266             addDynamicVars(info, child, name, ranges);
   267         }
   268     }
   269 
   270     private void addArgsNode(Node node, String name, ParserResult info, Set<OffsetRange> ranges) {
   271         ArgsNode an = (ArgsNode)node;
   272 
   273         if (an.getRequiredCount() > 0) {
   274             for (Node arg : an.childNodes()) {
   275                 if (!(arg instanceof ListNode)) continue;
   276                 for (Node arg2 : arg.childNodes()) {
   277                     if (arg2.getNodeType() == NodeType.ARGUMENTNODE) {
   278                         if (((ArgumentNode)arg2).getName().equals(name)) {
   279                             OffsetRange range = AstUtilities.getRange(arg2);
   280                             range = LexUtilities.getLexerOffsets(info, range);
   281                             if (range != OffsetRange.NONE) ranges.add(range);
   282                         }
   283                     } else if (arg2.getNodeType() == NodeType.LOCALASGNNODE) {
   284                         if (((LocalAsgnNode)arg2).getName().equals(name)) {
   285                             OffsetRange range = AstUtilities.getRange(arg2);
   286                             // Adjust end offset to only include the left hand size
   287                             range = new OffsetRange(range.getStart(), range.getStart() + name.length());
   288                             range = LexUtilities.getLexerOffsets(info, range);
   289                             if (range != OffsetRange.NONE) ranges.add(range);
   290                         }
   291                     }
   292                 }
   293             }
   294         }
   295 
   296         // Rest args
   297         if (an.getRest() != null) {
   298             ArgumentNode bn = an.getRest();
   299 
   300             if (bn.getName().equals(name)) {
   301                 SourcePosition pos = bn.getPosition();
   302 
   303                 // +1: Skip "*" and "&" prefix
   304                 OffsetRange range = new OffsetRange(pos.getStartOffset() + 1, pos.getEndOffset());
   305                 range = LexUtilities.getLexerOffsets(info, range);
   306                 if (range != OffsetRange.NONE) ranges.add(range);
   307             }
   308         }
   309 
   310         if (an.getBlock() != null) {
   311             BlockArgNode bn = an.getBlock();
   312 
   313             if (bn.getName().equals(name)) {
   314                 SourcePosition pos = bn.getPosition();
   315 
   316                 // +1: Skip "*" and "&" prefix
   317                 OffsetRange range = new OffsetRange(pos.getStartOffset() + 1, pos.getEndOffset());
   318                 range = LexUtilities.getLexerOffsets(info, range);
   319                 if (range != OffsetRange.NONE) ranges.add(range);
   320             }
   321         }
   322     }
   323 }