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.HashSet;
032: import java.util.List;
033: import java.util.Set;
034: import java.util.prefs.Preferences;
035: import javax.swing.Action;
036: import javax.swing.JComponent;
037: import javax.swing.SwingUtilities;
038: import org.jruby.ast.MethodDefNode;
039: import org.jruby.ast.Node;
040: import org.jruby.ast.NodeTypes;
041: import org.jruby.ast.types.INameNode;
042: import org.netbeans.modules.gsf.api.CompilationInfo;
043: import org.netbeans.modules.gsf.api.OffsetRange;
044: import org.netbeans.editor.BaseDocument;
045: import org.netbeans.modules.refactoring.api.ui.RefactoringActionsFactory;
046: import org.netbeans.modules.ruby.AstPath;
047: import org.netbeans.modules.ruby.AstUtilities;
048: import org.netbeans.modules.ruby.RubyUtils;
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.cookies.EditorCookie;
058: import org.openide.cookies.EditorCookie;
059: import org.openide.loaders.DataObject;
060: import org.openide.loaders.DataObjectNotFoundException;
061: import org.openide.util.Exceptions;
062: import org.openide.util.Lookup;
063: import org.openide.util.NbBundle;
064: import org.openide.util.lookup.AbstractLookup;
065: import org.openide.util.lookup.InstanceContent;
066:
067: /**
068: * Check names to see if they conform to standard Ruby conventions.
069: * Checks both method definitions and local symbols to see if they are
070: * using camel case rather than lowercase_names.
071: *
072: * @todo Add fix to rename!
073: *
074: * @author Tor Norbye
075: */
076: public class CamelCaseNames implements AstRule {
077: public CamelCaseNames() {
078: }
079:
080: public boolean appliesTo(CompilationInfo info) {
081: return true;
082: }
083:
084: public Set<Integer> getKinds() {
085: Set<Integer> integers = new HashSet<Integer>();
086: integers.add(NodeTypes.LOCALASGNNODE);
087: integers.add(NodeTypes.DEFNNODE);
088: integers.add(NodeTypes.DEFSNODE);
089: return integers;
090: }
091:
092: public void run(RuleContext context, List<Description> result) {
093: Node node = context.node;
094: CompilationInfo info = context.compilationInfo;
095:
096: String name = ((INameNode) node).getName();
097:
098: for (int i = 0; i < name.length(); i++) {
099: if (Character.isUpperCase(name.charAt(i))) {
100: String key = node.nodeId == NodeTypes.LOCALASGNNODE ? "InvalidLocalName"
101: : "InvalidMethodName"; // NOI18N
102: String displayName = NbBundle.getMessage(
103: CamelCaseNames.class, key);
104: OffsetRange range = AstUtilities.getNameRange(node);
105: range = LexUtilities.getLexerOffsets(info, range);
106: if (range != OffsetRange.NONE) {
107: List<Fix> fixList = new ArrayList<Fix>(2);
108: Node root = AstUtilities.getRoot(info);
109: AstPath childPath = new AstPath(root, node); // TODO - make a simple clone method to clone AstPath path
110: if (node.nodeId == NodeTypes.LOCALASGNNODE) {
111: fixList.add(new RenameFix(info, childPath,
112: RubyUtils.camelToUnderlinedName(name)));
113: }
114: fixList.add(new RenameFix(info, childPath, null));
115: Description desc = new Description(this ,
116: displayName, info.getFileObject(), range,
117: fixList, 1500);
118: result.add(desc);
119: }
120: return;
121: }
122: }
123: }
124:
125: public String getId() {
126: return "Camelcase_Names"; // NOI18N
127: }
128:
129: public String getDisplayName() {
130: return NbBundle.getMessage(CamelCaseNames.class,
131: "CamelCaseNames");
132: }
133:
134: public String getDescription() {
135: return NbBundle.getMessage(CamelCaseNames.class,
136: "CamelCaseNamesDesc");
137: }
138:
139: public boolean getDefaultEnabled() {
140: return true;
141: }
142:
143: public boolean showInTasklist() {
144: return true;
145: }
146:
147: public HintSeverity getDefaultSeverity() {
148: return HintSeverity.WARNING;
149: }
150:
151: public JComponent getCustomizer(Preferences node) {
152: return null;
153: }
154:
155: private static class RenameFix implements PreviewableFix, Runnable {
156:
157: private CompilationInfo info;
158: private AstPath path;
159: private String newName;
160:
161: RenameFix(CompilationInfo info, AstPath path, String newName) {
162: this .info = info;
163: this .path = path;
164: this .newName = newName;
165: }
166:
167: public String getDescription() {
168: if (newName != null) {
169: return NbBundle.getMessage(CamelCaseNames.class,
170: "RenameTo", newName);
171: } else {
172: return NbBundle.getMessage(CamelCaseNames.class,
173: "RenameVar");
174: }
175: }
176:
177: private Set<OffsetRange> getRanges() {
178: Node node = path.leaf();
179: assert node.nodeId == NodeTypes.LOCALASGNNODE;
180: String oldName = ((INameNode) node).getName();
181:
182: Node scope = AstUtilities.findLocalScope(node, path);
183: Set<OffsetRange> ranges = new HashSet<OffsetRange>();
184: addLocalRegions(scope, oldName, ranges);
185:
186: return ranges;
187: }
188:
189: private String getOldName() {
190: Node node = path.leaf();
191: assert node.nodeId == NodeTypes.LOCALASGNNODE;
192: String oldName = ((INameNode) node).getName();
193: return oldName;
194: }
195:
196: private EditList getEditList(String name) throws Exception {
197: int oldLength = getOldName().length();
198: Set<OffsetRange> ranges = getRanges();
199:
200: BaseDocument doc = (BaseDocument) info.getDocument();
201: EditList edits = new EditList(doc);
202:
203: for (OffsetRange range : ranges) {
204: edits.replace(range.getStart(), oldLength, name, false,
205: 0);
206: assert range.getLength() == oldLength;
207: }
208:
209: return edits;
210: }
211:
212: public boolean canPreview() {
213: return newName != null;
214: }
215:
216: public EditList getEditList() throws Exception {
217: return getEditList(newName != null ? newName : "new_name");
218: }
219:
220: public void implement() throws Exception {
221: if (newName != null) {
222: EditList edits = getEditList(newName);
223: edits.apply();
224: } else {
225: // Full rename
226: if (SwingUtilities.isEventDispatchThread()) {
227: run();
228: } else {
229: SwingUtilities.invokeLater(this );
230: }
231: }
232: }
233:
234: public void run() {
235: // Full rename - can only be done from the event dispatch thread
236: // (because the RefactoringActionsProvider calls getOpenedPanes on CloneableEditorSupport)
237: try {
238: DataObject od = DataObject.find(info.getFileObject());
239: EditorCookie ec = od.getCookie(EditorCookie.class);
240: org.openide.nodes.Node n = od.getNodeDelegate();
241: InstanceContent ic = new InstanceContent();
242: ic.add(ec);
243: ic.add(n);
244:
245: Lookup actionContext = new AbstractLookup(ic);
246: Action a = RefactoringActionsFactory.renameAction()
247: .createContextAwareInstance(actionContext);
248: a
249: .actionPerformed(RefactoringActionsFactory.DEFAULT_EVENT);
250: } catch (DataObjectNotFoundException dnf) {
251: Exceptions.printStackTrace(dnf);
252: }
253: }
254:
255: private void addLocalRegions(Node node, String name,
256: Set<OffsetRange> ranges) {
257: if ((node.nodeId == NodeTypes.LOCALASGNNODE || node.nodeId == NodeTypes.LOCALVARNODE)
258: && name.equals(((INameNode) node).getName())) {
259: OffsetRange range = AstUtilities.getNameRange(node);
260: range = LexUtilities.getLexerOffsets(info, range);
261: if (range != OffsetRange.NONE) {
262: ranges.add(range);
263: }
264: }
265:
266: @SuppressWarnings(value="unchecked")
267: List<Node> list = node.childNodes();
268:
269: for (Node child : list) {
270:
271: // Skip inline method defs
272: if (child instanceof MethodDefNode) {
273: continue;
274: }
275: addLocalRegions(child, name, ranges);
276: }
277: }
278:
279: public boolean isSafe() {
280: return false;
281: }
282:
283: public boolean isInteractive() {
284: return true;
285: }
286: }
287: }
|