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.Iterator;
034: import java.util.List;
035: import java.util.Map;
036: import java.util.Set;
037: import java.util.prefs.Preferences;
038: import javax.swing.JComponent;
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.NbUtilities;
050: import org.netbeans.modules.ruby.RubyParseResult;
051: import org.netbeans.modules.ruby.StructureAnalyzer.AnalysisResult;
052: import org.netbeans.modules.ruby.elements.AstAttributeElement;
053: import org.netbeans.modules.ruby.elements.AstClassElement;
054: import org.netbeans.modules.ruby.hints.spi.AstRule;
055: import org.netbeans.modules.ruby.hints.spi.Description;
056: import org.netbeans.modules.ruby.hints.spi.EditList;
057: import org.netbeans.modules.ruby.hints.spi.Fix;
058: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
059: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
060: import org.netbeans.modules.ruby.hints.spi.RuleContext;
061: import org.netbeans.modules.ruby.lexer.LexUtilities;
062: import org.openide.filesystems.FileObject;
063: import org.openide.util.NbBundle;
064:
065: /**
066: * Detect accidental local variable assignment intended to be an attribute call,
067: * such as
068: * <pre>
069: * class Foo
070: * attr_accessor :bar
071: *
072: * def foo
073: * bar = 50 # this does NOT change the bar property, shoudl be self.bar
074: * end
075: * end
076: * </pre>
077: *
078: *
079: *
080: * @author Tor Norbye
081: */
082: public class AttributeIsLocal implements AstRule {
083: public AttributeIsLocal() {
084: }
085:
086: private Map<AstClassElement, Set<AstAttributeElement>> attributes;
087: private Set<String> attributeNames;
088:
089: public boolean appliesTo(CompilationInfo info) {
090: RubyParseResult rpr = AstUtilities.getParseResult(info);
091: AnalysisResult ar = rpr.getStructure();
092: this .attributes = ar.getAttributes();
093:
094: if (attributes == null || attributes.size() == 0) {
095: return false;
096: }
097:
098: attributeNames = new HashSet<String>();
099: for (AstClassElement clz : attributes.keySet()) {
100: Set<AstAttributeElement> ats = attributes.get(clz);
101: for (AstAttributeElement ae : ats) {
102: if (!ae.isReadOnly()) {
103: attributeNames.add(ae.getName());
104: }
105: }
106: }
107:
108: return true;
109: }
110:
111: public Set<Integer> getKinds() {
112: return Collections.singleton(NodeTypes.LOCALASGNNODE);
113: }
114:
115: public void run(RuleContext context, List<Description> result) {
116: Node node = context.node;
117: AstPath path = context.path;
118: CompilationInfo info = context.compilationInfo;
119:
120: String name = ((INameNode) node).getName();
121: AstAttributeElement element = null;
122: if (attributeNames.contains(name)) {
123: // Possible clash! See if the class is right (the attribute could have been in another
124: // class than the one we're looking at)
125: Set<AstClassElement> keySet = attributes.keySet();
126: boolean match = false;
127: AstClassElement clzElement = null;
128: String fqn = AstUtilities.getFqnName(path);
129:
130: for (AstClassElement clz : keySet) {
131: if (fqn.equals(clz.getFqn())) {
132: clzElement = clz;
133: break;
134: }
135: }
136:
137: if (clzElement == null) {
138: return;
139: }
140:
141: match = false;
142: Set<AstAttributeElement> attribs = attributes
143: .get(clzElement);
144: if (attribs != null) {
145: for (AstAttributeElement ae : attribs) {
146: if (ae.getName().equals(name)) {
147: element = ae;
148: match = true;
149: break;
150: }
151: }
152: }
153:
154: if (!match) {
155: return;
156: }
157:
158: // Make sure it's not a parameter; these are not intended to access the attribute
159: // (e.g. example
160: // attr_accessor :nodoc
161: //
162: // def initialize(varname, types, inivalue, arraysuffix, comment,nodoc=false)
163: //
164: // In the above, nodoc=false is not a local assignment that should be this.nodoc=false
165: Iterator<Node> it = path.leafToRoot();
166: while (it.hasNext()) {
167: Node n = it.next();
168: if (n.nodeId == NodeTypes.ARGSNODE) {
169: return;
170: }
171: }
172:
173: assert element != null;
174: OffsetRange range = AstUtilities.getNameRange(node);
175: List<Fix> fixList = new ArrayList<Fix>(1);
176: fixList.add(new ShowAttributeFix(info, element));
177: fixList.add(new AttributeConflictFix(info, node, true));
178: fixList.add(new AttributeConflictFix(info, node, false));
179: range = LexUtilities.getLexerOffsets(info, range);
180: if (range != OffsetRange.NONE) {
181: Description desc = new Description(this ,
182: getDisplayName(), info.getFileObject(), range,
183: fixList, 50);
184: result.add(desc);
185: }
186: }
187: }
188:
189: public String getId() {
190: return "Attribute_Is_Local"; // NOI18N
191: }
192:
193: public String getDisplayName() {
194: return NbBundle.getMessage(AttributeIsLocal.class,
195: "AttributeIsLocal");
196: }
197:
198: public String getDescription() {
199: return NbBundle.getMessage(AttributeIsLocal.class,
200: "AttributeIsLocalDesc");
201: }
202:
203: public boolean getDefaultEnabled() {
204: return true;
205: }
206:
207: public HintSeverity getDefaultSeverity() {
208: return HintSeverity.WARNING;
209: }
210:
211: public boolean showInTasklist() {
212: return true;
213: }
214:
215: public JComponent getCustomizer(Preferences node) {
216: return null;
217: }
218:
219: private static class AttributeConflictFix implements PreviewableFix {
220:
221: private final CompilationInfo info;
222: private final boolean fixSelf;
223: private final Node node;
224:
225: AttributeConflictFix(CompilationInfo info, Node node,
226: boolean fixSelf) {
227: this .info = info;
228: this .node = node;
229: this .fixSelf = fixSelf;
230: }
231:
232: public String getDescription() {
233: return fixSelf ? NbBundle.getMessage(
234: AttributeIsLocal.class, "FixSelf",
235: ((INameNode) node).getName()) : NbBundle
236: .getMessage(AttributeIsLocal.class, "FixRename");
237: }
238:
239: public void implement() throws Exception {
240: EditList edits = createEditList(true);
241: if (edits != null) {
242: edits.apply();
243: }
244: }
245:
246: public EditList getEditList() throws Exception {
247: return createEditList(false);
248: }
249:
250: private EditList createEditList(boolean doit) throws Exception {
251: BaseDocument doc = (BaseDocument) info.getDocument();
252: EditList edits = new EditList(doc);
253:
254: if (fixSelf) {
255: OffsetRange range = AstUtilities.getRange(node);
256: int start = range.getStart();
257: start = LexUtilities.getLexerOffset(info, start);
258: if (start != -1) {
259: edits.replace(start, 0, "self.", false, 0); // NOI18N
260: }
261: } else {
262: // Initiate synchronous editing:
263: String name = ((INameNode) node).getName();
264: Node root = AstUtilities.getRoot(info);
265: AstPath path = new AstPath(root, node);
266: Node scope = AstUtilities.findLocalScope(path.leaf(),
267: path);
268: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
269: addLocalRegions(scope, name, ranges);
270: // Pick the first range as the caret offset
271: int caretOffset = Integer.MAX_VALUE;
272: for (OffsetRange range : ranges) {
273: if (range.getStart() < caretOffset) {
274: caretOffset = range.getStart();
275: }
276: }
277: if (doit) {
278: EditRegions.getInstance().edit(
279: info.getFileObject(), ranges, caretOffset);
280: return null;
281: } else {
282: String oldName = ((INameNode) path.leaf())
283: .getName();
284: int oldLen = oldName.length();
285: String newName = "new_name";
286: for (OffsetRange range : ranges) {
287: edits.replace(range.getStart(), oldLen,
288: newName, false, 0);
289: }
290: }
291: }
292:
293: return edits;
294: }
295:
296: private void addLocalRegions(Node node, String name,
297: Set<OffsetRange> ranges) {
298: if ((node.nodeId == NodeTypes.LOCALASGNNODE || node.nodeId == NodeTypes.LOCALVARNODE)
299: && name.equals(((INameNode) node).getName())) {
300: OffsetRange range = AstUtilities.getNameRange(node);
301: range = LexUtilities.getLexerOffsets(info, range);
302: if (range != OffsetRange.NONE) {
303: ranges.add(range);
304: }
305: }
306:
307: @SuppressWarnings(value="unchecked")
308: List<Node> list = node.childNodes();
309:
310: for (Node child : list) {
311:
312: // Skip inline method defs
313: if (child instanceof MethodDefNode) {
314: continue;
315: }
316: addLocalRegions(child, name, ranges);
317: }
318: }
319:
320: public boolean isSafe() {
321: return false;
322: }
323:
324: public boolean isInteractive() {
325: return true;
326: }
327:
328: public boolean canPreview() {
329: return true;
330: }
331: }
332:
333: private static class ShowAttributeFix implements Fix {
334:
335: private final CompilationInfo info;
336: private final AstAttributeElement element;
337:
338: ShowAttributeFix(CompilationInfo info,
339: AstAttributeElement element) {
340: this .info = info;
341: this .element = element;
342: }
343:
344: public String getDescription() {
345: Node creationNode = element.getCreationNode();
346: String desc;
347: if (creationNode instanceof INameNode) {
348: desc = ((INameNode) creationNode).getName() + " "
349: + element.getName(); // NOI18N
350: } else {
351: desc = element.getName();
352: }
353:
354: return NbBundle.getMessage(AttributeIsLocal.class,
355: "ShowAttribute", desc);
356: }
357:
358: public void implement() throws Exception {
359: FileObject fo = info.getFileObject();
360: int astOffset = element.getNode().getPosition()
361: .getStartOffset();
362: int lexOffset = LexUtilities
363: .getLexerOffset(info, astOffset);
364: if (lexOffset != -1) {
365: NbUtilities.open(fo, lexOffset, element.getName());
366: }
367: }
368:
369: public boolean isSafe() {
370: return true;
371: }
372:
373: public boolean isInteractive() {
374: return true;
375: }
376: }
377:
378: }
|