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: * If you wish your version of this file to be governed by only the CDDL
025: * or only the GPL Version 2, indicate your decision by adding
026: * "[Contributor] elects to include this software in this distribution
027: * under the [CDDL or GPL Version 2] license." If you do not indicate a
028: * single choice of license, a recipient has the option to distribute
029: * your version of this file under either the CDDL, the GPL Version 2 or
030: * to extend the choice of license to its licensees as provided above.
031: * However, if you add GPL Version 2 code and therefore, elected the GPL
032: * Version 2 license, then the option applies only if the new code is
033: * made subject to such option by the copyright holder.
034: *
035: * Contributor(s):
036: *
037: * Portions Copyrighted 2007 Sun Microsystems, Inc.
038: */
039:
040: package org.netbeans.modules.ruby.hints;
041:
042: import java.io.IOException;
043: import java.util.Collections;
044: import java.util.List;
045: import java.util.Set;
046: import java.util.prefs.Preferences;
047: import javax.swing.JComponent;
048: import javax.swing.text.BadLocationException;
049: import org.jruby.ast.IfNode;
050: import org.jruby.ast.Node;
051: import org.jruby.ast.NodeTypes;
052: import org.netbeans.modules.gsf.api.CompilationInfo;
053: import org.netbeans.modules.gsf.api.OffsetRange;
054: import org.netbeans.api.lexer.TokenHierarchy;
055: import org.netbeans.api.lexer.TokenSequence;
056: import org.netbeans.editor.BaseDocument;
057: import org.netbeans.editor.Utilities;
058: import org.netbeans.modules.ruby.AstPath;
059: import org.netbeans.modules.ruby.AstUtilities;
060: import org.netbeans.modules.ruby.RubyUtils;
061: import org.netbeans.modules.ruby.hints.spi.AstRule;
062: import org.netbeans.modules.ruby.hints.spi.Description;
063: import org.netbeans.modules.ruby.hints.spi.EditList;
064: import org.netbeans.modules.ruby.hints.spi.Fix;
065: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
066: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
067: import org.netbeans.modules.ruby.hints.spi.RuleContext;
068: import org.netbeans.modules.ruby.lexer.LexUtilities;
069: import org.openide.util.Exceptions;
070: import org.openide.util.NbBundle;
071:
072: /**
073: * Convert conditions of the form "if !foo" to "unless foo".
074: *
075: * Inspired by the excellent blog entry
076: * http://langexplr.blogspot.com/2007/11/creating-netbeans-ruby-hints-with-scala_24.html
077: * by Luis Diego Fallas.
078: *
079: * @todo Check ?: nodes
080: *
081: * @author Tor Norbye
082: */
083: public class ConvertIfToUnless implements AstRule {
084:
085: public Set<Integer> getKinds() {
086: return Collections.singleton(NodeTypes.IFNODE);
087: }
088:
089: public void run(RuleContext context, List<Description> result) {
090: Node node = context.node;
091: CompilationInfo info = context.compilationInfo;
092:
093: IfNode ifNode = (IfNode) node;
094:
095: // Convert unless blocks?
096: Node condition = ifNode.getCondition();
097: if (condition == null) {
098: // Can happen for this code:
099: // if ()
100: // end
101: // (typically while editing)
102: return;
103: }
104:
105: // Can't convert if !x/elseif blocks
106: if (ifNode.getElseBody() != null
107: && ifNode.getElseBody().nodeId == NodeTypes.IFNODE) {
108: return;
109: }
110:
111: if (condition.nodeId == NodeTypes.NOTNODE
112: || (condition.nodeId == NodeTypes.NEWLINENODE
113: && condition.childNodes().size() == 1 && ((Node) condition
114: .childNodes().get(0)).nodeId == NodeTypes.NOTNODE)) {
115: try {
116: BaseDocument doc = (BaseDocument) info.getDocument();
117: int keywordOffset = findKeywordOffset(info, ifNode);
118: if (keywordOffset == -1
119: || keywordOffset > doc.getLength() - 1) {
120: return;
121: }
122:
123: OffsetRange range = AstUtilities.getRange(node);
124:
125: if (RubyUtils.isRhtmlFile(info.getFileObject())) {
126: // Make sure that we're in a single contiguous Ruby section; if not, this won't work
127: range = LexUtilities.getLexerOffsets(info, range);
128: if (range == OffsetRange.NONE) {
129: return;
130: }
131:
132: try {
133: doc.readLock();
134: TokenHierarchy th = TokenHierarchy.get(doc);
135: TokenSequence ts = th.tokenSequence();
136: ts.move(range.getStart());
137: if (!ts.moveNext() && !ts.movePrevious()) {
138: return;
139: }
140:
141: if (ts.offset() + ts.token().length() < range
142: .getEnd()) {
143: return;
144: }
145: } finally {
146: if (doc != null) {
147: doc.readUnlock();
148: }
149: }
150: }
151:
152: ConvertToUnlessFix fix = new ConvertToUnlessFix(info,
153: ifNode);
154:
155: // Make sure we can actually perform the edit
156: if (fix.getEditList() == null) {
157: return;
158: }
159:
160: List<Fix> fixes = Collections.<Fix> singletonList(fix);
161:
162: String displayName = NbBundle.getMessage(
163: ConvertIfToUnless.class, "ConvertIfToUnless");
164: Description desc = new Description(this , displayName,
165: info.getFileObject(), range, fixes, 500);
166: result.add(desc);
167: } catch (BadLocationException ex) {
168: Exceptions.printStackTrace(ex);
169: } catch (IOException ex) {
170: Exceptions.printStackTrace(ex);
171: }
172: }
173: }
174:
175: public String getId() {
176: return "ConvertIfToUnless"; // NOI18N
177: }
178:
179: public String getDescription() {
180: return NbBundle.getMessage(ConvertIfToUnless.class,
181: "ConvertIfToUnlessDesc");
182: }
183:
184: public boolean getDefaultEnabled() {
185: return true;
186: }
187:
188: public JComponent getCustomizer(Preferences node) {
189: return null;
190: }
191:
192: public boolean appliesTo(CompilationInfo info) {
193: return true;
194: }
195:
196: public String getDisplayName() {
197: return NbBundle.getMessage(ConvertIfToUnless.class,
198: "ConvertIfToUnless");
199: }
200:
201: public boolean showInTasklist() {
202: return false;
203: }
204:
205: public HintSeverity getDefaultSeverity() {
206: return HintSeverity.CURRENT_LINE_WARNING;
207: }
208:
209: static int findKeywordOffset(CompilationInfo info, IfNode ifNode)
210: throws IOException, BadLocationException {
211: BaseDocument doc = (BaseDocument) info.getDocument();
212:
213: int astIfOffset = ifNode.getPosition().getStartOffset();
214: int lexIfOffset = LexUtilities
215: .getLexerOffset(info, astIfOffset);
216: if (lexIfOffset == -1 || lexIfOffset > doc.getLength()) {
217: return -1;
218: }
219:
220: String statement = doc.getText(lexIfOffset, 2);
221: if (statement.equals("if")) {
222: // Make sure it's not "elsif"
223: if (lexIfOffset > 3) {
224: statement = doc.getText(lexIfOffset - 3, 5);
225: if ("elsif".equals(statement)) {
226: return -1;
227: }
228: }
229: return lexIfOffset;
230: } else if (statement.equals("un")) {
231: return lexIfOffset;
232: } else {
233: // Probably a statement modifier - gotta adjust the if offset
234: int conditionStart = LexUtilities.getLexerOffset(info,
235: AstUtilities.getRange(ifNode.getCondition())
236: .getStart());
237: int lineStart = Utilities.getRowFirstNonWhite(doc,
238: conditionStart);
239: if (lineStart != -1 && lineStart < conditionStart) {
240: String line = doc.getText(lineStart,
241: conditionStart - lineStart).trim();
242: if (line.endsWith("elsif")) { // NO!I8N
243: // Can't perform conversions on elsif!
244: return -1;
245: }
246: if (line.endsWith("if")) { // NOI18N
247: return lineStart + line.length() - 2;
248: } else if (line.endsWith("unless")) { // NOI18N
249: return lineStart + line.length() - 6;
250: }
251: }
252: }
253:
254: return -1;
255: }
256:
257: private class ConvertToUnlessFix implements PreviewableFix {
258: private CompilationInfo info;
259: private IfNode ifNode;
260:
261: public ConvertToUnlessFix(CompilationInfo info, IfNode ifNode) {
262: this .info = info;
263: this .ifNode = ifNode;
264: }
265:
266: public String getDescription() {
267: String lif = "if"; // NOI18N
268: String lunless = "unless"; // NOI18N
269: String from;
270: String to;
271: if (ifNode.getThenBody() != null) {
272: from = lif;
273: to = lunless;
274: } else {
275: from = lunless;
276: to = lif;
277: }
278: return NbBundle.getMessage(ConvertIfToUnless.class,
279: "ConvertIfToUnlessFix", from, to);
280: }
281:
282: public void implement() throws Exception {
283: EditList edits = getEditList();
284: if (edits != null) {
285: edits.apply();
286: }
287: }
288:
289: public EditList getEditList() {
290: try {
291: BaseDocument doc = (BaseDocument) info.getDocument();
292:
293: Node notNode = ifNode.getCondition();
294: if (notNode.nodeId != NodeTypes.NOTNODE) {
295: assert notNode.nodeId == NodeTypes.NEWLINENODE;
296: Node firstChild = notNode.childNodes().size() == 1 ? ((Node) notNode
297: .childNodes().get(0))
298: : null;
299: if (firstChild != null
300: && firstChild.nodeId == NodeTypes.NOTNODE) {
301: notNode = firstChild;
302: } else {
303: // Unexpected!
304: assert false : firstChild;
305: return null;
306: }
307:
308: }
309:
310: int astNotOffset = AstUtilities.getRange(notNode)
311: .getStart();
312: int lexNotOffset = LexUtilities.getLexerOffset(info,
313: astNotOffset);
314: if (lexNotOffset == -1
315: || lexNotOffset > doc.getLength() - 1) {
316: return null;
317: }
318:
319: int astIfOffset = ifNode.getPosition().getStartOffset();
320: int lexIfOffset = LexUtilities.getLexerOffset(info,
321: astIfOffset);
322: if (lexIfOffset == -1 || lexIfOffset > doc.getLength()) {
323: return null;
324: }
325:
326: boolean isEqualComparison = false;
327: char c = doc.getText(lexNotOffset, 1).charAt(0);
328: if (c != '!') {
329: // Probably something like "!=", where the not node range points to
330: // a call node calling method "=="
331: int lineEnd = Utilities
332: .getRowEnd(doc, lexNotOffset);
333: String line = doc.getText(lexNotOffset, lineEnd
334: - lexNotOffset);
335: int lineOffset = line.indexOf("!=");
336: if (lineOffset == -1) {
337: assert false : line;
338: return null;
339: } else {
340: lexNotOffset += lineOffset;
341: isEqualComparison = true;
342: }
343: }
344:
345: int keywordOffset = findKeywordOffset(info, ifNode);
346: if (keywordOffset == -1
347: || keywordOffset > doc.getLength() - 1) {
348: return null;
349: }
350:
351: assert keywordOffset < lexNotOffset;
352:
353: char k = doc.getText(keywordOffset, 1).charAt(0);
354: boolean isIf = k == 'i';
355:
356: EditList edits = new EditList(doc);
357:
358: if (isEqualComparison) {
359: // Convert != into ==
360: edits.replace(lexNotOffset, 1, "=", false, 0);
361: } else {
362: // Just remove ! from the expression
363: edits.replace(lexNotOffset, 1, null, false, 0);
364: }
365: if (isIf) {
366: edits.replace(keywordOffset, 2, "unless", false, 1);
367: } else {
368: edits.replace(keywordOffset, 6, "if", false, 1);
369: }
370:
371: return edits;
372: } catch (Exception ex) {
373: Exceptions.printStackTrace(ex);
374: return null;
375: }
376: }
377:
378: public boolean isSafe() {
379: return true;
380: }
381:
382: public boolean isInteractive() {
383: return false;
384: }
385:
386: public boolean canPreview() {
387: return true;
388: }
389: }
390: }
|