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.LocalAsgnNode;
038: import org.jruby.ast.LocalVarNode;
039: import org.jruby.ast.MethodDefNode;
040: import org.jruby.ast.Node;
041: import org.jruby.ast.NodeTypes;
042: import org.jruby.ast.types.INameNode;
043: import org.netbeans.modules.gsf.api.CompilationInfo;
044: import org.netbeans.modules.gsf.api.EditRegions;
045: import org.netbeans.modules.gsf.api.OffsetRange;
046: import org.netbeans.editor.BaseDocument;
047: import org.netbeans.modules.ruby.AstPath;
048: import org.netbeans.modules.ruby.AstUtilities;
049: import org.netbeans.modules.ruby.hints.spi.AstRule;
050: import org.netbeans.modules.ruby.hints.spi.Description;
051: import org.netbeans.modules.ruby.hints.spi.EditList;
052: import org.netbeans.modules.ruby.hints.spi.Fix;
053: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
054: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
055: import org.netbeans.modules.ruby.hints.spi.RuleContext;
056: import org.netbeans.modules.ruby.lexer.LexUtilities;
057: import org.openide.util.NbBundle;
058:
059: /**
060: * A hint which looks for block variables that are reusing a variable
061: * that already exists in scope when the block is initiated.
062: * This will (-possibly- unintentionally) reassign the local variable
063: * so might be something the user want to be alerted to
064: *
065: * @todo Doesn't seem to work for params
066: * @todo Don't warn on last lines?
067: *
068: * @author Tor Norbye
069: */
070: public class NestedLocal implements AstRule {
071:
072: public NestedLocal() {
073: }
074:
075: public boolean appliesTo(CompilationInfo info) {
076: return true;
077: }
078:
079: public Set<Integer> getKinds() {
080: return Collections.singleton(NodeTypes.FORNODE);
081: }
082:
083: public void cancel() {
084: // Does nothing
085: }
086:
087: public String getId() {
088: return "Nested_Local"; // NOI18N
089: }
090:
091: public String getDisplayName() {
092: return NbBundle.getMessage(NestedLocal.class, "NestedLocal");
093: }
094:
095: public String getDescription() {
096: return NbBundle
097: .getMessage(NestedLocal.class, "NestedLocalDesc");
098: }
099:
100: public void run(RuleContext context, List<Description> result) {
101: Node node = context.node;
102: AstPath path = context.path;
103: CompilationInfo info = context.compilationInfo;
104:
105: if (node.nodeId == NodeTypes.FORNODE) {
106: // Check the children and see if we have a LocalAsgnNode; tbese are the
107: // loop variables which are NOT local to the for block; if found, go and see
108: // if it's a reuse!
109: @SuppressWarnings(value="unchecked")
110: List<Node> list = node.childNodes();
111:
112: for (Node child : list) {
113: if (child instanceof LocalAsgnNode) {
114: String name = ((INameNode) child).getName();
115: Node method = AstUtilities.findLocalScope(null,
116: path);
117: if (method != null
118: && isUsed(method, name, child,
119: new boolean[1])) {
120: OffsetRange range = AstUtilities
121: .getNameRange(child);
122: List<Fix> fixList = new ArrayList<Fix>(2);
123: Node root = AstUtilities.getRoot(info);
124: AstPath childPath = new AstPath(root, child);
125: fixList.add(new RenameVarFix(info, childPath,
126: node, false));
127: fixList.add(new RenameVarFix(info, childPath,
128: node, true));
129:
130: // TODO - add a hint to turn off this hint?
131: // Should be a utility or infrastructure option!
132: String displayName = NbBundle.getMessage(
133: NestedLocal.class, "NestedLocalName",
134: name);
135:
136: range = LexUtilities.getLexerOffsets(info,
137: range);
138: if (range != OffsetRange.NONE) {
139: Description desc = new Description(this ,
140: displayName, info.getFileObject(),
141: range, fixList, 100);
142: result.add(desc);
143: }
144: }
145: }
146: }
147: }
148: }
149:
150: private boolean isUsed(Node node, String name, Node target,
151: boolean[] done) {
152: if (node == target) {
153: done[0] = true;
154: return false;
155: }
156:
157: if (node.nodeId == NodeTypes.LOCALVARNODE
158: || node.nodeId == NodeTypes.LOCALASGNNODE) {
159: if (name.equals(((INameNode) node).getName())) {
160: return true;
161: }
162: }
163:
164: @SuppressWarnings(value="unchecked")
165: List<Node> list = node.childNodes();
166:
167: for (Node child : list) {
168: boolean found = isUsed(child, name, target, done);
169:
170: if (found) {
171: return true;
172: }
173:
174: if (done[0]) {
175: return false;
176: }
177: }
178:
179: return false;
180: }
181:
182: private static class RenameVarFix implements PreviewableFix {
183:
184: private CompilationInfo info;
185:
186: private AstPath path;
187: private Node target;
188:
189: private boolean renameOuter;
190:
191: RenameVarFix(CompilationInfo info, AstPath path, Node target,
192: boolean renameOuter) {
193: this .info = info;
194: this .target = target;
195: this .path = path;
196: this .renameOuter = renameOuter;
197: }
198:
199: public String getDescription() {
200: if (renameOuter) {
201: return NbBundle.getMessage(NestedLocal.class,
202: "ChangeOuter");
203: } else {
204: return NbBundle.getMessage(NestedLocal.class,
205: "ChangeInner");
206: }
207: }
208:
209: public EditList getEditList() throws Exception {
210: BaseDocument doc = (BaseDocument) info.getDocument();
211: EditList edits = new EditList(doc);
212: Set<OffsetRange> ranges = findRegionsToEdit();
213: String oldName = ((INameNode) path.leaf()).getName();
214: int oldLen = oldName.length();
215: String newName = "new_name";
216: for (OffsetRange range : ranges) {
217: edits.replace(range.getStart(), oldLen, newName, false,
218: 0);
219: }
220: return edits;
221: }
222:
223: public void implement() throws Exception {
224: // Refactoring isn't necessary here since local variables and block
225: // variables are limited to the local scope, so we can accurately just
226: // find their positions using the AST and let the user edit them synchronously.
227: Set<OffsetRange> ranges = findRegionsToEdit();
228:
229: // Pick the first range as the caret offset
230: int caretOffset = Integer.MAX_VALUE;
231: for (OffsetRange range : ranges) {
232: if (range.getStart() < caretOffset) {
233: caretOffset = range.getStart();
234: }
235: }
236:
237: // Initiate synchronous editing:
238: EditRegions.getInstance().edit(info.getFileObject(),
239: ranges, caretOffset);
240: }
241:
242: private void addNonBlockRefs(Node node, String name,
243: Set<OffsetRange> ranges) {
244: if (((node instanceof LocalAsgnNode) || (node instanceof LocalVarNode))
245: && name.equals(((INameNode) node).getName())) {
246: OffsetRange range = AstUtilities.getNameRange(node);
247: range = LexUtilities.getLexerOffsets(info, range);
248: if (range != OffsetRange.NONE) {
249: ranges.add(range);
250: }
251: }
252:
253: @SuppressWarnings(value="unchecked")
254: List<Node> list = node.childNodes();
255:
256: for (Node child : list) {
257:
258: // Skip inline method defs
259: if (child instanceof MethodDefNode) {
260: continue;
261: }
262:
263: if (child == target) {
264: // prune the block
265: continue;
266: }
267:
268: addNonBlockRefs(child, name, ranges);
269: }
270: }
271:
272: private Set<OffsetRange> findRegionsToEdit() {
273: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
274:
275: assert path.leaf() instanceof INameNode;
276: String name = ((INameNode) path.leaf()).getName();
277:
278: if (renameOuter) {
279: Node scope = AstUtilities.findLocalScope(path.leaf(),
280: path);
281: addNonBlockRefs(scope, name, ranges);
282: } else {
283: addNonBlockRefs(target, name, ranges);
284: }
285:
286: return ranges;
287: }
288:
289: public boolean isSafe() {
290: return false;
291: }
292:
293: public boolean isInteractive() {
294: return true;
295: }
296:
297: public boolean canPreview() {
298: return true;
299: }
300: }
301:
302: public boolean getDefaultEnabled() {
303: return true;
304: }
305:
306: public HintSeverity getDefaultSeverity() {
307: return HintSeverity.WARNING;
308: }
309:
310: public boolean showInTasklist() {
311: return true;
312: }
313:
314: public JComponent getCustomizer(Preferences node) {
315: return null;
316: }
317: }
|