001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * Portions Copyrighted 2007 Sun Microsystems, Inc.
027: */
028: package org.netbeans.modules.ruby.hints;
029:
030: import java.util.ArrayList;
031: import java.util.Collections;
032: import java.util.HashSet;
033: import java.util.List;
034: import java.util.Set;
035: import java.util.prefs.Preferences;
036: import javax.swing.JComponent;
037: import org.jruby.ast.IterNode;
038: import org.jruby.ast.Node;
039: import org.jruby.ast.NodeTypes;
040: import org.jruby.ast.types.INameNode;
041: import org.netbeans.modules.gsf.api.CompilationInfo;
042: import org.netbeans.modules.gsf.api.EditRegions;
043: import org.netbeans.modules.gsf.api.OffsetRange;
044: import org.netbeans.editor.BaseDocument;
045: import org.netbeans.modules.ruby.AstPath;
046: import org.netbeans.modules.ruby.AstUtilities;
047: import org.netbeans.modules.ruby.hints.spi.AstRule;
048: import org.netbeans.modules.ruby.hints.spi.Description;
049: import org.netbeans.modules.ruby.hints.spi.EditList;
050: import org.netbeans.modules.ruby.hints.spi.Fix;
051: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
052: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
053: import org.netbeans.modules.ruby.hints.spi.RuleContext;
054: import org.netbeans.modules.ruby.lexer.LexUtilities;
055: import org.openide.util.NbBundle;
056:
057: /**
058: * A hint which looks for block variables that are reusing a variable
059: * that already exists in scope when the block is initiated.
060: * This will (-possibly- unintentionally) reassign the local variable
061: * so might be something the user want to be alerted to
062: *
063: * @todo Doesn't seem to work for params
064: * @todo Don't warn on last lines?
065: * @todo Doesn't seem to work for comma-separated lists of arguments, e.g. { |foo,bar| }
066: * (at least not the second arg)
067: *
068: * @author Tor Norbye
069: */
070: public class BlockVarReuse implements AstRule {
071:
072: public BlockVarReuse() {
073: }
074:
075: public boolean appliesTo(CompilationInfo info) {
076: return true;
077: }
078:
079: public Set<Integer> getKinds() {
080: return Collections.singleton(NodeTypes.ITERNODE);
081: }
082:
083: public void cancel() {
084: // Does nothing
085: }
086:
087: public String getId() {
088: return "Block_Var_Reuse"; // NOI18N
089: }
090:
091: public String getDisplayName() {
092: return NbBundle.getMessage(BlockVarReuse.class,
093: "UnintentionalSideEffect");
094: }
095:
096: public String getDescription() {
097: return NbBundle.getMessage(BlockVarReuse.class,
098: "UnintentionalSideEffectDesc");
099: }
100:
101: public void run(RuleContext context, List<Description> result) {
102: Node node = context.node;
103: CompilationInfo info = context.compilationInfo;
104:
105: if (node.nodeId == NodeTypes.ITERNODE) {
106: // Check the children and see if we have a LocalAsgnNode; these are going
107: // to be local variable reuses
108: @SuppressWarnings(value="unchecked")
109: List<Node> list = node.childNodes();
110:
111: for (Node child : list) {
112: if (child.nodeId == NodeTypes.LOCALASGNNODE) {
113:
114: OffsetRange range = AstUtilities
115: .getNameRange(child);
116: List<Fix> fixList = new ArrayList<Fix>(2);
117: Node root = AstUtilities.getRoot(info);
118: AstPath childPath = new AstPath(root, child);
119: fixList
120: .add(new RenameVarFix(info, childPath,
121: false));
122: fixList
123: .add(new RenameVarFix(info, childPath, true));
124:
125: range = LexUtilities.getLexerOffsets(info, range);
126: if (range != OffsetRange.NONE) {
127: Description desc = new Description(this ,
128: getDisplayName(), info.getFileObject(),
129: range, fixList, 100);
130: result.add(desc);
131: }
132: }
133: }
134: }
135: }
136:
137: private static class RenameVarFix implements PreviewableFix {
138:
139: private CompilationInfo info;
140:
141: private AstPath path;
142:
143: private boolean renameLocal;
144:
145: RenameVarFix(CompilationInfo info, AstPath path,
146: boolean renameLocal) {
147: this .info = info;
148: this .path = path;
149: this .renameLocal = renameLocal;
150: }
151:
152: public String getDescription() {
153: if (renameLocal) {
154: return NbBundle.getMessage(BlockVarReuse.class,
155: "ChangeLocalVarName");
156: } else {
157: return NbBundle.getMessage(BlockVarReuse.class,
158: "ChangeBlockVarName");
159: }
160: }
161:
162: public void implement() throws Exception {
163: // Refactoring isn't necessary here since local variables and block
164: // variables are limited to the local scope, so we can accurately just
165: // find their positions using the AST and let the user edit them synchronously.
166: Set<OffsetRange> ranges = findRegionsToEdit();
167:
168: // Pick the first range as the caret offset
169: int caretOffset = Integer.MAX_VALUE;
170: for (OffsetRange range : ranges) {
171: if (range.getStart() < caretOffset) {
172: caretOffset = range.getStart();
173: }
174: }
175:
176: // Initiate synchronous editing:
177: EditRegions.getInstance().edit(info.getFileObject(),
178: ranges, caretOffset);
179: }
180:
181: private void addNonBlockRefs(Node node, String name,
182: Set<OffsetRange> ranges) {
183: if ((node.nodeId == NodeTypes.LOCALASGNNODE || node.nodeId == NodeTypes.LOCALVARNODE)
184: && name.equals(((INameNode) node).getName())) {
185: OffsetRange range = AstUtilities.getNameRange(node);
186: range = LexUtilities.getLexerOffsets(info, range);
187: if (range != OffsetRange.NONE) {
188: ranges.add(range);
189: }
190: }
191:
192: @SuppressWarnings(value="unchecked")
193: List<Node> list = node.childNodes();
194:
195: for (Node child : list) {
196:
197: // Skip inline method defs
198: if (child.nodeId == NodeTypes.DEFNNODE
199: || child.nodeId == NodeTypes.DEFSNODE) {
200: continue;
201: }
202:
203: // Skip SOME blocks:
204: if (child.nodeId == NodeTypes.ITERNODE) {
205: // Skip only the block which the fix is applying to;
206: // the local variable could be aliased in other blocks as well
207: // and we need to keep the program correct!
208: if (child == path.leafParent()) {
209:
210: continue;
211: }
212: }
213:
214: addNonBlockRefs(child, name, ranges);
215: }
216: }
217:
218: private Set<OffsetRange> findRegionsToEdit() {
219: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
220:
221: assert path.leaf() instanceof INameNode;
222: String name = ((INameNode) path.leaf()).getName();
223:
224: if (renameLocal) {
225: Node scope = AstUtilities.findLocalScope(path.leaf(),
226: path);
227: addNonBlockRefs(scope, name, ranges);
228: } else {
229: Node parent = path.leafParent();
230: assert parent instanceof IterNode;
231: addNonBlockRefs(parent, name, ranges);
232: }
233:
234: return ranges;
235: }
236:
237: public boolean isSafe() {
238: return false;
239: }
240:
241: public boolean isInteractive() {
242: return true;
243: }
244:
245: public EditList getEditList() throws Exception {
246: BaseDocument doc = (BaseDocument) info.getDocument();
247: EditList edits = new EditList(doc);
248: Set<OffsetRange> ranges = findRegionsToEdit();
249: String oldName = ((INameNode) path.leaf()).getName();
250: int oldLen = oldName.length();
251: String newName = "new_name";
252: for (OffsetRange range : ranges) {
253: edits.replace(range.getStart(), oldLen, newName, false,
254: 0);
255: }
256: return edits;
257: }
258:
259: public boolean canPreview() {
260: return true;
261: }
262: }
263:
264: public boolean getDefaultEnabled() {
265: return true;
266: }
267:
268: public HintSeverity getDefaultSeverity() {
269: return HintSeverity.WARNING;
270: }
271:
272: public boolean showInTasklist() {
273: return true;
274: }
275:
276: public JComponent getCustomizer(Preferences node) {
277: return null;
278: }
279: }
|