ruby.refactoring/src/org/netbeans/modules/refactoring/ruby/plugins/RenameRefactoringPlugin.java
Bump jruby-parser and hopefully see green (commented out tests pass individually -- some state surviving to kill them later -- workaround for now)
2 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
4 * Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
6 * Oracle and Java are registered trademarks of Oracle and/or its affiliates.
7 * Other names may be trademarks of their respective owners.
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]"
29 * The Original Software is NetBeans. The Initial Developer of the Original
30 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
31 * Microsystems, Inc. All Rights Reserved.
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.
44 package org.netbeans.modules.refactoring.ruby.plugins;
46 import java.io.IOException;
47 import java.text.MessageFormat;
49 import java.util.logging.Level;
50 import java.util.logging.Logger;
51 import javax.swing.text.BadLocationException;
52 import javax.swing.text.Document;
53 import javax.swing.text.Position.Bias;
54 import org.jrubyparser.ast.ArgumentNode;
55 import org.jrubyparser.ast.ClassNode;
56 import org.jrubyparser.ast.ClassVarAsgnNode;
57 import org.jrubyparser.ast.ClassVarDeclNode;
58 import org.jrubyparser.ast.ClassVarNode;
59 import org.jrubyparser.ast.Colon2Node;
60 import org.jrubyparser.ast.DAsgnNode;
61 import org.jrubyparser.ast.DVarNode;
62 import org.jrubyparser.ast.GlobalAsgnNode;
63 import org.jrubyparser.ast.GlobalVarNode;
64 import org.jrubyparser.ast.InstAsgnNode;
65 import org.jrubyparser.ast.InstVarNode;
66 import org.jrubyparser.ast.LocalAsgnNode;
67 import org.jrubyparser.ast.LocalVarNode;
68 import org.jrubyparser.ast.MethodDefNode;
69 import org.jrubyparser.ast.ModuleNode;
70 import org.jrubyparser.ast.Node;
71 import org.jrubyparser.ast.SClassNode;
72 import org.jrubyparser.ast.SymbolNode;
73 import org.jrubyparser.ast.INameNode;
74 import org.netbeans.api.lexer.Token;
75 import org.netbeans.api.lexer.TokenHierarchy;
76 import org.netbeans.api.lexer.TokenId;
77 import org.netbeans.api.lexer.TokenSequence;
78 import org.netbeans.api.lexer.TokenUtilities;
79 import org.netbeans.api.project.FileOwnerQuery;
80 import org.netbeans.api.project.Project;
81 import org.netbeans.editor.BaseDocument;
82 import org.netbeans.editor.Utilities;
83 import org.netbeans.modules.csl.api.ElementKind;
84 import org.netbeans.modules.csl.api.Error;
85 import org.netbeans.modules.csl.api.OffsetRange;
86 import org.netbeans.modules.csl.api.Severity;
87 import org.netbeans.modules.csl.spi.ParserResult;
88 import org.netbeans.modules.csl.spi.support.ModificationResult;
89 import org.netbeans.modules.csl.spi.support.ModificationResult.Difference;
90 import org.netbeans.modules.parsing.api.ParserManager;
91 import org.netbeans.modules.parsing.api.ResultIterator;
92 import org.netbeans.modules.parsing.api.Source;
93 import org.netbeans.modules.parsing.api.UserTask;
94 import org.netbeans.modules.parsing.spi.ParseException;
95 import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport.Kind;
96 import org.netbeans.modules.refactoring.api.*;
97 import org.netbeans.modules.refactoring.ruby.DiffElement;
98 import org.netbeans.modules.refactoring.ruby.RetoucheUtils;
99 import org.netbeans.modules.refactoring.ruby.RubyElementCtx;
100 import org.netbeans.modules.refactoring.spi.RefactoringElementsBag;
101 import org.netbeans.modules.ruby.AstPath;
102 import org.netbeans.modules.ruby.AstUtilities;
103 import org.netbeans.modules.ruby.RubyIndex;
104 import org.netbeans.modules.ruby.RubyParseResult;
105 import org.netbeans.modules.ruby.RubyStructureAnalyzer.AnalysisResult;
106 import org.netbeans.modules.ruby.RubyUtils;
107 import org.netbeans.modules.ruby.elements.AstElement;
108 import org.netbeans.modules.ruby.elements.Element;
109 import org.netbeans.modules.ruby.elements.IndexedClass;
110 import org.netbeans.modules.ruby.elements.IndexedElement;
111 import org.netbeans.modules.ruby.elements.IndexedMethod;
112 import org.netbeans.modules.ruby.lexer.LexUtilities;
113 import org.netbeans.modules.ruby.rubyproject.RubyBaseProject;
114 import org.openide.filesystems.FileObject;
115 import org.openide.filesystems.FileUtil;
116 import org.openide.loaders.OperationEvent.Rename;
117 import org.openide.text.CloneableEditorSupport;
118 import org.openide.text.PositionRef;
119 import org.openide.util.Exceptions;
120 import org.openide.util.NbBundle;
123 * The actual Renaming refactoring work for Ruby. The skeleton (name checks etc.) based
124 * on the Java refactoring module by Jan Becicka, Martin Matula, Pavel Flaska and Daniel Prusa.
126 * @author Jan Becicka
127 * @author Martin Matula
128 * @author Pavel Flaska
129 * @author Daniel Prusa
132 * @todo Perform index lookups to determine the set of files to be checked!
133 * @todo Check that the new name doesn't conflict with an existing name
134 * @todo Check unknown files!
135 * @todo More prechecks
136 * @todo When invoking refactoring on a file object, I also rename the file. I should (a) list the
137 * name it's going to change the file to, and (b) definitely "filenamize" it - e.g. for class FooBar the
138 * filename should be foo_bar.
139 * @todo If you rename a Model, I should add a corresponding rename_table entry in the migrations...
141 * @todo Complete this. Most of the prechecks are not implemented - and the refactorings themselves need a lot of work.
143 public class RenameRefactoringPlugin extends RubyRefactoringPlugin {
145 private RubyElementCtx treePathHandle;
146 private final Collection<IndexedMethod> overriddenByMethods = new ArrayList<IndexedMethod>();
147 private final Collection<IndexedMethod> overridesMethods = new ArrayList<IndexedMethod>();; // methods that are overridden by the method to be renamed
148 // private boolean doCheckName = true;
150 private RenameRefactoring refactoring;
151 private RubyBaseProject project;
153 /** Creates a new instance of RenameRefactoring */
154 public RenameRefactoringPlugin(RenameRefactoring rename) {
155 this.refactoring = rename;
156 RubyElementCtx tph = rename.getRefactoringSource().lookup(RubyElementCtx.class);
158 treePathHandle = tph;
160 Source source = Source.create(rename.getRefactoringSource().lookup(FileObject.class));
162 ParserManager.parse(Collections.singleton(source), new UserTask() {
166 void run(ResultIterator co) throws Exception {
167 if (co.getSnapshot().getMimeType().equals(RubyUtils.RUBY_MIME_TYPE)) {
168 RubyParseResult parserResult = AstUtilities.getParseResult(co.getParserResult());
169 org.jrubyparser.ast.Node root = parserResult.getRootNode();
171 AnalysisResult ar = parserResult.getStructure();
172 List<? extends AstElement> els = ar.getElements();
173 if (els.size() > 0) {
174 // TODO - try to find the outermost or most "relevant" module/class in the file?
175 // In Java, we look for a class with the name corresponding to the file.
176 // It's not as simple in Ruby.
177 AstElement element = els.get(0);
178 org.jrubyparser.ast.Node node = element.getNode();
179 treePathHandle = new RubyElementCtx(root, node,
180 element, RubyUtils.getFileObject(parserResult), parserResult);
181 refactoring.getContext().add(co);
187 } catch (ParseException e) {
188 Logger.getLogger(RenameRefactoringPlugin.class.getName()).log(Level.WARNING, null, e);
191 if (treePathHandle != null) {
192 Project p = FileOwnerQuery.getOwner(treePathHandle.getFileObject());
193 if (p instanceof RubyBaseProject) {
194 project = (RubyBaseProject) p;
199 public Problem fastCheckParameters() {
200 Problem fastCheckProblem = null;
201 ElementKind kind = treePathHandle.getKind();
202 String newName = refactoring.getNewName();
203 String oldName = treePathHandle.getSimpleName();
204 if (oldName == null) {
205 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)");
208 if (oldName.equals(newName)) {
209 boolean nameNotChanged = true;
210 //if (kind == ElementKind.CLASS || kind == ElementKind.MODULE) {
211 // if (!((TypeElement) element).getNestingKind().isNested()) {
212 // nameNotChanged = info.getFileObject().getName().equals(element);
215 if (nameNotChanged) {
216 fastCheckProblem = createProblem(fastCheckProblem, true, getString("ERR_NameNotChanged"));
217 return fastCheckProblem;
222 // TODO - get a better ruby name picker - and check for invalid Ruby symbol names etc.
223 // TODO - call RubyUtils.isValidLocalVariableName if we're renaming a local symbol!
224 if (kind == ElementKind.CLASS && !RubyUtils.isValidConstantFQN(newName)) {
225 String s = getString("ERR_InvalidClassName"); //NOI18N
226 String msg = new MessageFormat(s).format(
227 new Object[] {newName}
229 fastCheckProblem = createProblem(fastCheckProblem, true, msg);
230 return fastCheckProblem;
231 } else if (kind == ElementKind.METHOD && !RubyUtils.isValidRubyMethodName(newName)) {
232 String s = getString("ERR_InvalidMethodName"); //NOI18N
233 String msg = new MessageFormat(s).format(
234 new Object[] {newName}
236 fastCheckProblem = createProblem(fastCheckProblem, true, msg);
237 return fastCheckProblem;
238 } else if (!RubyUtils.isValidRubyIdentifier(newName)) {
239 String s = getString("ERR_InvalidIdentifier"); //NOI18N
240 String msg = new MessageFormat(s).format(
241 new Object[] {newName}
243 fastCheckProblem = createProblem(fastCheckProblem, true, msg);
244 return fastCheckProblem;
246 String msg = getWarningMsg(kind, newName);
248 fastCheckProblem = createProblem(fastCheckProblem, false, msg);
251 return fastCheckProblem;
254 private Set<String> asNames(Collection<? extends IndexedElement> elems) {
255 Set<String> names = new HashSet<String>(elems.size());
256 for (IndexedElement each : elems) {
257 names.add(each.getName());
262 public Problem checkParameters() {
264 Problem checkProblem = null;
266 if (AstUtilities.isCall(treePathHandle.getNode()) || treePathHandle.getKind() == ElementKind.METHOD) {
267 RubyIndex index = RubyIndex.get(treePathHandle.getInfo());
268 String className = treePathHandle.getDefClass();
269 String methodName = AstUtilities.getName(treePathHandle.getNode());
270 Set<IndexedMethod> methodsInSameTree = index.getAllOverridingMethodsInHierachy(methodName, className);
271 overridesMethods.addAll(methodsInSameTree);
273 // inherited contains also the method itself
274 if (overridesMethods.size() > 1) {
275 Set<String> superClassNames = asNames(index.getSuperClasses(className));
276 // does the method override a super method that is defined in a class in the project sources
277 boolean overridesFromSources = false;
278 // does the method overrided a super method that is also overridden in a class in a
279 // different branch of the class hierarhcy
280 boolean classesInOtherBranch = false;
282 for (IndexedMethod method : overridesMethods) {
283 // warn about matches under non-source roots (we don't rename them)
284 if (!isUnderSourceRoot(method.getFileObject())) {
286 createProblem(checkProblem,
287 false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides_Method",
288 method.getIn() + "#" + method.getName(), method.getFileObject().getPath()));
289 } else if (!method.getFileObject().equals(treePathHandle.getFileObject())){
290 overridesFromSources = true;
292 if (!classesInOtherBranch
293 && !className.equals(method.getIn())
294 && !superClassNames.contains(method.getIn())) {
295 classesInOtherBranch = true;
298 if (overridesFromSources) {
299 checkProblem = createProblem(checkProblem, false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides"));
301 if (classesInOtherBranch) {
302 checkProblem = createProblem(checkProblem, false, NbBundle.getMessage(RenameRefactoringPlugin.class, "ERR_Overrides_tree"));
307 steps += overriddenByMethods.size();
308 steps += overridesMethods.size();
310 fireProgressListenerStart(RenameRefactoring.PARAMETERS_CHECK, 8 + 3*steps);
312 fireProgressListenerStep();
313 fireProgressListenerStep();
314 fireProgressListenerStop();
318 private boolean isUnderSourceRoot(FileObject fo) {
319 if (project == null) {
322 for (FileObject root : project.getSourceRootFiles()) {
323 if (FileUtil.isParentOf(root, fo)) {
327 for (FileObject root : project.getTestSourceRootFiles()) {
328 if (FileUtil.isParentOf(root, fo)) {
336 public Problem preCheck() {
337 if (treePathHandle == null || treePathHandle.getFileObject() == null || !treePathHandle.getFileObject().isValid()) {
338 return new Problem(true, NbBundle.getMessage(RenameRefactoringPlugin.class, "DSC_ElNotAvail")); // NOI18N
343 private Set<FileObject> getRelevantFiles() {
344 if (treePathHandle.getKind() == ElementKind.VARIABLE || treePathHandle.getKind() == ElementKind.PARAMETER) {
345 // For local variables, only look in the current file!
346 return Collections.singleton(treePathHandle.getFileObject());
348 return RetoucheUtils.getRubyFilesInProject(treePathHandle.getFileObject());
353 private Set<RubyElementCtx> allMethods;
355 private static final Comparator<Difference> COMPARATOR = new Comparator<Difference>() {
356 public int compare(Difference d1, Difference d2) {
357 return d1.getStartPosition().getOffset() - d2.getStartPosition().getOffset();
362 public Problem prepare(RefactoringElementsBag elements) {
363 if (treePathHandle == null) {
366 Problem problem = null;
367 Set<FileObject> files = getRelevantFiles();
368 fireProgressListenerStart(ProgressEvent.START, files.size());
369 if (!files.isEmpty()) {
370 TransformTask transform = new TransformTask() {
372 protected Collection<ModificationResult> process(ParserResult parserResult) {
373 RenameTransformer rt = new RenameTransformer(refactoring.getNewName(), allMethods);
374 rt.setWorkingCopy(parserResult);
376 ModificationResult mr = new ModificationResult();
378 mr.addDifferences(parserResult.getSnapshot().getSource().getFileObject(), cullDifferences(rt.diffs));
380 return Collections.singleton(mr);
384 final Collection<ModificationResult> results = processFiles(files, transform);
386 // We don't want retouche to look at all results since we are finding the same nodes
387 // repeated in our tree (e.g. @a += 1 has two nodes for @a for the assign and the references).
388 for (ModificationResult result: results) {
389 for (FileObject jfo : result.getModifiedFileObjects()) {
391 for (Difference diff: result.getDifferences(jfo)) {
392 String old = diff.getOldText();
393 if (old!=null) { //TODO: workaround. generator issue?
394 elements.add(refactoring,DiffElement.create(diff, jfo, result));
400 elements.registerTransaction(new RetoucheCommit(results));
402 // see #126733. need to set a correct new name for the file rename plugin
403 // that gets invoked after this plugin when the refactoring is invoked on a file.
404 if (refactoring.getRefactoringSource().lookup(FileObject.class) != null) {
405 String newName = RubyUtils.camelToUnderlinedName(refactoring.getNewName());
406 refactoring.setNewName(newName);
409 fireProgressListenerStop();
414 // At NB 8.0 it gets really unhappy if we submit duplicate Differences which overlap.
415 // This method sorts and then removes any which happen to have the same start offset.
416 // Start offset might not be perfect but in the cases on improper overlaps it should get
417 // repaired by the thing supplying the differences.
418 private static List<Difference> cullDifferences(List<Difference> oldDiffs) {
419 List<Difference> diffs = new ArrayList<Difference>();
420 Difference lastDiff = null;
422 if (oldDiffs.size() > 0) Collections.sort(oldDiffs, COMPARATOR);
424 for (Difference diff: oldDiffs) {
425 if (lastDiff == null ||
426 (diff.getStartPosition().getOffset() != lastDiff.getStartPosition().getOffset() &&
427 diff.getEndPosition().getOffset() != lastDiff.getEndPosition().getOffset())) {
437 private static final String getString(String key) {
438 return NbBundle.getMessage(RenameRefactoringPlugin.class, key);
441 private String getWarningMsg(ElementKind kind, String newName) {
443 if (ElementKind.CLASS == kind) {
444 for (String each : newName.split("::")) {
446 msg = RubyUtils.getIdentifierWarning(each, 0);
452 msg = RubyUtils.getIdentifierWarning(newName, 0);
459 * @author Jan Becicka
461 public class RenameTransformer extends SearchVisitor {
463 private final Set<RubyElementCtx> allMethods;
464 private final String newName;
465 private final String oldName;
466 private CloneableEditorSupport ces;
467 private List<Difference> diffs;
470 public void setWorkingCopy(ParserResult workingCopy) {
471 // Cached per working copy
474 super.setWorkingCopy(workingCopy);
477 public RenameTransformer(String newName, Set<RubyElementCtx> am) {
478 this.newName = newName;
479 this.oldName = treePathHandle.getSimpleName();
480 this.allMethods = am;
485 // TODO - do I need to force state to resolved?
486 //compiler.toPhase(org.netbeans.napi.gsfret.source.Phase.RESOLVED);
488 diffs = new ArrayList<Difference>();
489 RubyElementCtx searchCtx = treePathHandle;
491 Node root = AstUtilities.getRoot(workingCopy);
492 FileObject workingCopyFileObject = RubyUtils.getFileObject(workingCopy);
495 Element element = AstElement.create(workingCopy, root);
496 Node node = searchCtx.getNode();
497 RubyElementCtx fileCtx = new RubyElementCtx(root, node, element, workingCopyFileObject, workingCopy);
499 if (node instanceof ArgumentNode) {
500 AstPath path = searchCtx.getPath();
501 assert path.leaf() == node;
502 Node parent = path.leafParent();
504 if (!(parent instanceof MethodDefNode)) {
505 method = AstUtilities.findLocalScope(node, path);
507 } else if (node instanceof LocalVarNode || node instanceof LocalAsgnNode || node instanceof DAsgnNode ||
508 node instanceof DVarNode) {
509 // A local variable read or a parameter read, or an assignment to one of these
510 AstPath path = searchCtx.getPath();
511 method = AstUtilities.findLocalScope(node, path);
514 if (method != null) {
515 findLocal(searchCtx, fileCtx, method, oldName);
518 AstPath path = new AstPath();
520 find(path, searchCtx, fileCtx, root, oldName, Character.isUpperCase(oldName.charAt(0)));
524 // See if the document contains references to this symbol and if so, put a warning in
525 String workingCopyText = workingCopy.getSnapshot().getText().toString();
527 if (workingCopyText.indexOf(oldName) != -1) {
530 ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
534 String desc = NbBundle.getMessage(RenameRefactoringPlugin.class, "ParseErrorFile", oldName);
535 List<? extends Error> errors = workingCopy.getDiagnostics();
536 if (errors.size() > 0) {
537 for (Error e : errors) {
538 if (e.getSeverity() == Severity.ERROR) {
544 error = errors.get(0);
547 String errorMsg = error.getDisplayName();
549 if (errorMsg.length() > 80) {
550 errorMsg = errorMsg.substring(0, 77) + "..."; // NOI18N
553 desc = desc + "; " + errorMsg;
554 start = error.getStartPosition();
555 start = LexUtilities.getLexerOffset(workingCopy, start);
561 PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
562 PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
563 Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, "", "", desc); // NOI18N
568 if (error == null && refactoring.isSearchInComments()) {
569 Document doc = RetoucheUtils.getDocument(workingCopy, RubyUtils.getFileObject(workingCopy));
572 TokenHierarchy<Document> th = TokenHierarchy.get(doc);
573 TokenSequence<?> ts = th.tokenSequence();
577 searchTokenSequence(ts);
584 private void searchTokenSequence(TokenSequence<?> ts) {
587 Token<?> token = ts.token();
588 TokenId id = token.id();
590 String primaryCategory = id.primaryCategory();
591 if ("comment".equals(primaryCategory) || "block-comment".equals(primaryCategory)) { // NOI18N
592 // search this comment
593 CharSequence tokenText = token.text();
594 if (tokenText == null || oldName == null) {
597 int index = TokenUtilities.indexOf(tokenText, oldName);
599 String text = tokenText.toString();
600 // TODO make sure it's its own word. Technically I could
601 // look at identifier chars like "_" here but since they are
602 // used for other purposes in comments, consider letters
603 // and numbers as enough
604 if ((index == 0 || !Character.isLetterOrDigit(text.charAt(index-1))) &&
605 (index+oldName.length() >= text.length() ||
606 !Character.isLetterOrDigit(text.charAt(index+oldName.length())))) {
607 int start = ts.offset() + index;
608 int end = start + oldName.length();
610 ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
612 PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
613 PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
614 String desc = getString("ChangeComment");
615 Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, oldName, newName, desc);
620 TokenSequence<?> embedded = ts.embedded();
621 if (embedded != null) {
622 searchTokenSequence(embedded);
625 } while (ts.moveNext());
629 private void rename(Node node, String oldCode, String newCode, String desc) {
630 OffsetRange range = AstUtilities.getNameRange(node);
631 assert range != OffsetRange.NONE;
632 int pos = range.getStart();
635 // TODO - insert "method call", "method definition", "class definition", "symbol", "attribute" etc. and from and too?
636 if (node instanceof MethodDefNode) {
637 desc = getString("UpdateMethodDef");
638 } else if (AstUtilities.isCall(node)) {
639 desc = getString("UpdateCall");
640 } else if (node instanceof SymbolNode) {
641 desc = getString("UpdateSymbol");
642 } else if (node instanceof ClassNode || node instanceof SClassNode) {
643 desc = getString("UpdateClassDef");
644 } else if (node instanceof ModuleNode) {
645 desc = getString("UpdateModule");
646 } else if (node instanceof LocalVarNode || node instanceof LocalAsgnNode || node instanceof DVarNode || node instanceof DAsgnNode) {
647 desc = getString("UpdateLocalvar");
648 } else if (node instanceof GlobalVarNode || node instanceof GlobalAsgnNode) {
649 desc = getString("UpdateGlobal");
650 } else if (node instanceof InstVarNode || node instanceof InstAsgnNode) {
651 desc = getString("UpdateInstance");
652 } else if (node instanceof ClassVarNode || node instanceof ClassVarDeclNode || node instanceof ClassVarAsgnNode) {
653 desc = getString("UpdateClassvar");
655 desc = NbBundle.getMessage(RenameRefactoringPlugin.class, "UpdateRef", oldCode);
660 ces = RetoucheUtils.findCloneableEditorSupport(workingCopy);
663 // Convert from AST to lexer offsets if necessary
664 pos = LexUtilities.getLexerOffset(workingCopy, pos);
666 // Translation failed
671 int end = pos+oldCode.length();
672 // TODO if a SymbolNode, +=1 since the symbolnode includes the ":"
673 BaseDocument doc = null;
675 doc = (BaseDocument)ces.openDocument();
678 if (start > doc.getLength()) {
679 start = end = doc.getLength();
682 if (end > doc.getLength()) {
683 end = doc.getLength();
686 // Look in the document and search around a bit to detect the exact method reference
687 // (and adjust position accordingly). Thus, if I have off by one errors in the AST (which
688 // occasionally happens) the user's source won't get munged
689 if (!oldCode.equals(doc.getText(start, end-start))) {
690 // Look back and forwards by 1 at first
691 int lineStart = Utilities.getRowFirstNonWhite(doc, start);
692 int lineEnd = Utilities.getRowLastNonWhite(doc, start)+1; // +1: after last char
693 if (lineStart == -1 || lineEnd == -1) { // We're really on the wrong line!
694 System.out.println("Empty line entry in " + FileUtil.getFileDisplayName(RubyUtils.getFileObject(workingCopy)) +
695 "; no match for " + oldCode + " in line " + start + " referenced by node " +
696 node + " of type " + node.getClass().getName());
700 if (lineStart < 0 || lineEnd-lineStart < 0) {
701 return; // Can't process this one
704 String line = doc.getText(lineStart, lineEnd-lineStart);
705 if (line.indexOf(oldCode) == -1) {
706 System.out.println("Skipping entry in " + FileUtil.getFileDisplayName(RubyUtils.getFileObject(workingCopy)) +
707 "; no match for " + oldCode + " in line " + line + " referenced by node " +
708 node + " of type " + node.getClass().getName());
710 int lineOffset = start-lineStart;
712 // Search up and down by one
713 for (int distance = 1; distance < line.length(); distance++) {
715 if (lineOffset+distance+oldCode.length() <= line.length() &&
716 oldCode.equals(line.substring(lineOffset+distance, lineOffset+distance+oldCode.length()))) {
717 newOffset = lineOffset+distance;
720 if (lineOffset-distance >= 0 && lineOffset-distance+oldCode.length() <= line.length() &&
721 oldCode.equals(line.substring(lineOffset-distance, lineOffset-distance+oldCode.length()))) {
722 newOffset = lineOffset-distance;
727 if (newOffset != -1) {
728 start = newOffset+lineStart;
729 end = start+oldCode.length();
733 } catch (IOException ie) {
734 Exceptions.printStackTrace(ie);
735 } catch (BadLocationException ble) {
736 Exceptions.printStackTrace(ble);
743 if (newCode == null) {
744 // Usually it's the new name so allow client code to refer to it as just null
745 newCode = refactoring.getNewName(); // XXX isn't this == our field "newName"?
748 PositionRef startPos = ces.createPositionRef(start, Bias.Forward);
749 PositionRef endPos = ces.createPositionRef(end, Bias.Forward);
750 Difference diff = new Difference(Difference.Kind.CHANGE, startPos, endPos, oldCode, newCode, desc);
754 /** Search for local variables in local scope */
755 private void findLocal(RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name) {
756 switch (node.getNodeType()) {
758 // TODO - check parent and make sure it's not a method of the same name?
759 // e.g. if I have "def foo(foo)" and I'm searching for "foo" (the parameter),
760 // I don't want to pick up the ArgumentNode under def foo that corresponds to the
761 // "foo" method name!
762 if (((ArgumentNode)node).getName().equals(name)) {
763 rename(node, name, null, getString("RenameParam"));
766 // I don't have alias nodes within a method, do I?
767 // } else if (node instanceof AliasNode) {
768 // AliasNode an = (AliasNode)node;
769 // if (an.getNewName().equals(name) || an.getOldName().equals(name)) {
770 // elements.add(refactoring, WhereUsedElement.create(matchCtx));
775 if (((INameNode)node).getName().equals(name)) {
776 rename(node, name, null, getString("UpdateLocalvar"));
781 if (((INameNode)node).getName().equals(name)) {
782 // Found a method call match
783 // TODO - make a node on the same line
784 // TODO - check arity - see OccurrencesFinder
785 rename(node, name, null, getString("UpdateDynvar"));
789 // XXX Can I have symbols to local variables? Try it!!!
790 if (((SymbolNode)node).getName().equals(name)) {
791 rename(node, name, null, getString("UpdateSymbol"));
796 for (Node child : node.childNodes()) {
797 findLocal(searchCtx, fileCtx, child, name);
802 * @todo P1: This is matching method names on classes that have nothing to do with the class we're searching for
803 * - I've gotta filter fields, methods etc. that are not in the current class
804 * (but I also have to search for methods that are OVERRIDING the class... so I've gotta work a little harder!)
805 * @todo Arity matching on the methods to preclude methods that aren't overriding or aliasing!
807 @SuppressWarnings("fallthrough")
808 private void find(AstPath path, RubyElementCtx searchCtx, RubyElementCtx fileCtx, Node node, String name, boolean upperCase) {
809 /* TODO look for both old and new and attempt to fix
810 if (node instanceof AliasNode) {
811 AliasNode an = (AliasNode)node;
812 if (an.getNewName().equals(name) || an.getOldName().equals(name)) {
813 RubyElementCtx matchCtx = new RubyElementCtx(fileCtx, node);
814 elements.add(refactoring, WhereUsedElement.create(matchCtx));
816 } else*/ if (!upperCase) {
817 // Local variables - I can be smarter about context searches here!
819 // Methods, attributes, etc.
820 // TODO - be more discriminating on the filetype
821 switch (node.getNodeType()) {
824 if (((MethodDefNode)node).getName().equals(name)) {
826 boolean skip = false;
828 // Check that we're in a class or module we're interested in
829 String fqn = AstUtilities.getFqnName(path);
830 if (fqn == null || fqn.length() == 0) {
831 fqn = RubyIndex.OBJECT;
834 if (!fqn.equals(searchCtx.getDefClass())) {
835 boolean inherited = false;
836 for (IndexedMethod method : overridesMethods) {
837 if (method.getIn().equals(fqn)) {
842 // XXX THE ABOVE IS NOT RIGHT - I shouldn't
843 // use equals on the class names, I should use the
844 // index and see if one derives fromor includes the other
849 if (!skip && AstUtilities.isCall(searchCtx.getNode())) {
850 // The reference is a call and this is a definition; see if
851 // this looks like a match
852 // TODO - enforce that this method is also in the desired
854 if (!AstUtilities.isCallFor(searchCtx.getNode(), searchCtx.getArity(), node)) {
858 // The search handle is a method def, as is this, with the same name.
859 // Now I need to go and see if this is an override (e.g. compatible
865 // Found a method match
866 // TODO - check arity - see OccurrencesFinder
867 node = ((MethodDefNode)node).getNameNode();
868 rename(node, name, null, getString("UpdateMethodDef"));
874 if (AstUtilities.isAttr(node)) {
875 SymbolNode[] symbols = AstUtilities.getAttrSymbols(node);
876 for (SymbolNode symbol : symbols) {
877 if (symbol.getName().equals(name)) {
878 // TODO - can't replace the whole node here - I need to replace only the text!
879 rename(node, name, null, null);
883 // Fall through for other call checking
886 if (((INameNode)node).getName().equals(name)) {
887 // TODO - if it's a call without a lhs (e.g. Call.LOCAL),
888 // make sure that we're referring to the same method call
889 // Found a method call match
890 // TODO - make a node on the same line
891 // TODO - check arity - see OccurrencesFinder
892 rename(node, name, null, null);
896 if (((SymbolNode)node).getName().equals(name)) {
897 // TODO do something about the colon?
898 rename(node, name, null, null);
906 case CLASSVARASGNNODE:
907 case CLASSVARDECLNODE:
908 if (((INameNode)node).getName().equals(name)) {
909 rename(node, ((INameNode)node).getLexicalName(), null, null);
914 // Classes, modules, constants, etc.
915 switch (node.getNodeType()) {
917 Colon2Node c2n = (Colon2Node)node;
918 if (c2n.getName().equals(name)) {
919 rename(node, name, null, null);
926 if (((INameNode)node).getName().equals(name)) {
927 rename(node, name, null, null);
933 for (Node child : node.childNodes()) {
935 find(path, searchCtx, fileCtx, child, name, upperCase);