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.infrastructure;
029:
030: import java.io.IOException;
031: import java.util.ArrayList;
032: import java.util.Collection;
033: import java.util.Collections;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.LinkedList;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.Map.Entry;
040: import org.jruby.ast.Node;
041: import org.jruby.ast.NodeTypes;
042: import org.netbeans.modules.gsf.api.CompilationInfo;
043: import org.netbeans.modules.gsf.api.Error;
044: import org.netbeans.modules.gsf.api.HintsProvider;
045: import org.netbeans.modules.gsf.api.OffsetRange;
046: import org.netbeans.modules.gsf.api.ParserResult;
047: import org.netbeans.editor.BaseDocument;
048: import org.netbeans.modules.ruby.AstPath;
049: import org.netbeans.modules.ruby.AstUtilities;
050: import org.netbeans.modules.ruby.RubyMimeResolver;
051: import org.netbeans.modules.ruby.hints.options.HintsSettings;
052: import org.netbeans.modules.ruby.hints.spi.AstRule;
053: import org.netbeans.modules.ruby.hints.spi.Description;
054: import org.netbeans.modules.ruby.hints.spi.ErrorRule;
055: import org.netbeans.modules.ruby.hints.spi.HintSeverity;
056: import org.netbeans.modules.ruby.hints.spi.PreviewableFix;
057: import org.netbeans.modules.ruby.hints.spi.Rule;
058: import org.netbeans.modules.ruby.hints.spi.RuleContext;
059: import org.netbeans.modules.ruby.hints.spi.SelectionRule;
060: import org.netbeans.modules.ruby.hints.spi.UserConfigurableRule;
061: import org.netbeans.spi.editor.hints.ChangeInfo;
062: import org.netbeans.spi.editor.hints.EnhancedFix;
063: import org.netbeans.spi.editor.hints.ErrorDescription;
064: import org.netbeans.spi.editor.hints.ErrorDescriptionFactory;
065: import org.netbeans.spi.editor.hints.Fix;
066: import org.openide.util.Exceptions;
067:
068: /**
069: * Class which acts on the rules and suggestions by iterating the
070: * AST and invoking applicable rules
071: *
072: *
073: * @author Tor Norbye
074: */
075: public class RubyHintsProvider implements HintsProvider {
076: private boolean cancelled;
077: private Map<Integer, List<AstRule>> testHints;
078: private Map<Integer, List<AstRule>> testSuggestions;
079: private List<SelectionRule> testSelectionHints;
080: private Map<String, List<ErrorRule>> testErrors;
081:
082: public RubyHintsProvider() {
083: }
084:
085: private boolean isTest() {
086: return testHints != null || testSuggestions != null
087: || testSelectionHints != null || testErrors != null;
088: }
089:
090: public List<Error> computeErrors(CompilationInfo info,
091: List<ErrorDescription> result) {
092: try {
093: if (info.getDocument() == null) {
094: // Document probably closed
095: return Collections.emptyList();
096: }
097: } catch (IOException ex) {
098: Exceptions.printStackTrace(ex);
099: }
100:
101: Collection<? extends ParserResult> embeddedResults = info
102: .getEmbeddedResults(RubyMimeResolver.RUBY_MIME_TYPE);
103: if (embeddedResults.size() == 0) {
104: return Collections.emptyList();
105: }
106:
107: assert embeddedResults.size() == 1; // I don't create multiple discontiguous ruby sections
108: ParserResult parserResult = embeddedResults.iterator().next();
109:
110: List<Error> errors = parserResult.getDiagnostics();
111: if (errors == null || errors.size() == 0) {
112: return Collections.emptyList();
113: }
114:
115: cancelled = false;
116:
117: Map<String, List<ErrorRule>> hints = testErrors;
118: if (hints == null) {
119: hints = RulesManager.getInstance().getErrors();
120: }
121:
122: if (hints.isEmpty() || isCancelled()) {
123: return errors;
124: }
125:
126: List<Description> descriptions = new ArrayList<Description>();
127:
128: List<Error> unhandled = new ArrayList<Error>();
129:
130: for (Error error : errors) {
131: if (!applyRules(error, info, hints, descriptions)) {
132: unhandled.add(error);
133: }
134: }
135:
136: if (descriptions.size() > 0) {
137: for (Description desc : descriptions) {
138: ErrorDescription errorDesc = createDescription(desc,
139: info, -1);
140: result.add(errorDesc);
141: }
142: }
143:
144: return unhandled;
145: }
146:
147: public void computeSelectionHints(CompilationInfo info,
148: List<ErrorDescription> result, int start, int end) {
149: try {
150: if (info.getDocument() == null) {
151: // Document probably closed
152: return;
153: }
154: } catch (IOException ex) {
155: Exceptions.printStackTrace(ex);
156: }
157:
158: cancelled = false;
159:
160: Node root = AstUtilities.getRoot(info);
161:
162: if (root == null) {
163: return;
164: }
165: List<SelectionRule> hints = testSelectionHints;
166: if (hints == null) {
167: hints = RulesManager.getInstance().getSelectionHints();
168: }
169:
170: if (hints.isEmpty()) {
171: return;
172: }
173:
174: if (isCancelled()) {
175: return;
176: }
177:
178: List<Description> descriptions = new ArrayList<Description>();
179:
180: applyRules(info, hints, start, end, descriptions);
181:
182: if (descriptions.size() > 0) {
183: for (Description desc : descriptions) {
184: ErrorDescription errorDesc = createDescription(desc,
185: info, -1);
186: result.add(errorDesc);
187: }
188: }
189: }
190:
191: public void computeHints(CompilationInfo info,
192: List<ErrorDescription> result) {
193: try {
194: if (info.getDocument() == null) {
195: // Document probably closed
196: return;
197: }
198: } catch (IOException ex) {
199: Exceptions.printStackTrace(ex);
200: }
201:
202: cancelled = false;
203:
204: Node root = AstUtilities.getRoot(info);
205:
206: if (root == null) {
207: return;
208: }
209: Map<Integer, List<AstRule>> hints = testHints;
210: if (hints == null) {
211: hints = RulesManager.getInstance().getHints(false, info);
212: }
213:
214: if (hints.isEmpty()) {
215: return;
216: }
217:
218: if (isCancelled()) {
219: return;
220: }
221:
222: List<Description> descriptions = new ArrayList<Description>();
223:
224: AstPath path = new AstPath();
225: path.descend(root);
226:
227: applyRules(NodeTypes.ROOTNODE, root, path, info, hints, -1,
228: descriptions);
229:
230: scan(root, path, info, hints, -1, descriptions);
231: path.ascend();
232:
233: if (descriptions.size() > 0) {
234: for (Description desc : descriptions) {
235: ErrorDescription errorDesc = createDescription(desc,
236: info, -1);
237: result.add(errorDesc);
238: }
239: }
240: }
241:
242: private ErrorDescription createDescription(Description desc,
243: CompilationInfo info, int caretPos) {
244: Rule rule = desc.getRule();
245: HintSeverity severity;
246: if (rule instanceof UserConfigurableRule) {
247: severity = RulesManager.getInstance().getSeverity(
248: (UserConfigurableRule) rule);
249: } else {
250: severity = rule.getDefaultSeverity();
251: }
252: OffsetRange range = desc.getRange();
253: List<Fix> fixList;
254: if (desc.getFixes() != null && desc.getFixes().size() > 0) {
255: fixList = new ArrayList<Fix>(desc.getFixes().size());
256:
257: // TODO print out priority with left flushed 0's here
258: // this is just a hack
259: String sortText = Integer.toString(10000 + desc
260: .getPriority());
261:
262: for (org.netbeans.modules.ruby.hints.spi.Fix fix : desc
263: .getFixes()) {
264: fixList.add(new FixWrapper(fix, sortText));
265:
266: if (fix instanceof PreviewableFix) {
267: PreviewableFix previewFix = (PreviewableFix) fix;
268: if (previewFix.canPreview() && !isTest()) {
269: fixList.add(new PreviewHintFix(info,
270: previewFix, sortText));
271: }
272: }
273: }
274:
275: if (rule instanceof UserConfigurableRule && !isTest()) {
276: // Add a hint for disabling this fix
277: fixList.add(new DisableHintFix(
278: (UserConfigurableRule) rule, info, caretPos,
279: sortText));
280: }
281: } else {
282: fixList = Collections.emptyList();
283: }
284: return ErrorDescriptionFactory.createErrorDescription(severity
285: .toEditorSeverity(), desc.getDescription(), fixList,
286: desc.getFile(), range.getStart(), range.getEnd());
287:
288: }
289:
290: public void computeSuggestions(CompilationInfo info,
291: List<ErrorDescription> result, int caretOffset) {
292: try {
293: if (info.getDocument() == null) {
294: // Document probably closed
295: return;
296: }
297: } catch (IOException ex) {
298: Exceptions.printStackTrace(ex);
299: }
300:
301: cancelled = false;
302:
303: Node root = AstUtilities.getRoot(info);
304:
305: if (root == null) {
306: return;
307: }
308:
309: Map<Integer, List<AstRule>> suggestions = testSuggestions;
310: if (suggestions == null) {
311: suggestions = new HashMap<Integer, List<AstRule>>();
312:
313: suggestions.putAll(RulesManager.getInstance().getHints(
314: true, info));
315:
316: for (Entry<Integer, List<AstRule>> e : RulesManager
317: .getInstance().getSuggestions().entrySet()) {
318: List<AstRule> rules = suggestions.get(e.getKey());
319:
320: if (rules != null) {
321: List<AstRule> res = new LinkedList<AstRule>();
322:
323: res.addAll(rules);
324: res.addAll(e.getValue());
325:
326: suggestions.put(e.getKey(), res);
327: } else {
328: suggestions.put(e.getKey(), e.getValue());
329: }
330: }
331: }
332:
333: if (suggestions.isEmpty()) {
334: return;
335: }
336:
337: if (isCancelled()) {
338: return;
339: }
340:
341: int astOffset = AstUtilities.getAstOffset(info, caretOffset);
342:
343: AstPath path = new AstPath(root, astOffset);
344: List<Description> descriptions = new ArrayList<Description>();
345:
346: Iterator<Node> it = path.leafToRoot();
347: while (it.hasNext()) {
348: if (isCancelled()) {
349: return;
350: }
351:
352: Node node = it.next();
353: applyRules(node.nodeId, node, path, info, suggestions,
354: caretOffset, descriptions);
355: }
356:
357: //applyRules(NodeTypes.ROOTNODE, path, info, suggestions, caretOffset, result);
358:
359: if (descriptions.size() > 0) {
360: for (Description desc : descriptions) {
361: ErrorDescription errorDesc = createDescription(desc,
362: info, caretOffset);
363: result.add(errorDesc);
364: }
365: }
366: }
367:
368: private void applyRules(int nodeType, Node node, AstPath path,
369: CompilationInfo info, Map<Integer, List<AstRule>> hints,
370: int caretOffset, List<Description> result) {
371: List<AstRule> rules = hints.get(nodeType);
372:
373: if (rules != null) {
374: RuleContext context = new RuleContext();
375: context.compilationInfo = info;
376: try {
377: context.doc = (BaseDocument) info.getDocument();
378: if (context.doc == null) {
379: // Document closed
380: return;
381: }
382: } catch (IOException ex) {
383: Exceptions.printStackTrace(ex);
384: }
385: context.node = node;
386: context.path = path;
387: context.caretOffset = caretOffset;
388:
389: for (AstRule rule : rules) {
390: if (HintsSettings.isEnabled(rule)) {
391: rule.run(context, result);
392: }
393: }
394: }
395: }
396:
397: /** Apply error rules and return true iff somebody added an error description for it */
398: private boolean applyRules(Error error, CompilationInfo info,
399: Map<String, List<ErrorRule>> hints, List<Description> result) {
400: String code = error.getKey();
401: if (code != null) {
402: List<ErrorRule> rules = hints.get(code);
403:
404: if (rules != null) {
405: int countBefore = result.size();
406: RuleContext context = new RuleContext();
407: context.compilationInfo = info;
408: try {
409: context.doc = (BaseDocument) info.getDocument();
410: if (context.doc == null) {
411: // Document closed
412: return false;
413: }
414: } catch (IOException ex) {
415: Exceptions.printStackTrace(ex);
416: }
417:
418: for (ErrorRule rule : rules) {
419: if (!rule.appliesTo(info)) {
420: continue;
421: }
422: rule.run(context, error, result);
423: }
424:
425: return countBefore < result.size();
426: }
427: }
428:
429: return false;
430: }
431:
432: private void applyRules(CompilationInfo info,
433: List<SelectionRule> rules, int start, int end,
434: List<Description> result) {
435:
436: RuleContext context = new RuleContext();
437: context.compilationInfo = info;
438: //context.node = node;
439: //context.path = path;
440: context.selectionStart = start;
441: context.selectionEnd = end;
442: try {
443: context.doc = (BaseDocument) info.getDocument();
444: if (context.doc == null) {
445: // Document closed
446: return;
447: }
448: } catch (IOException ex) {
449: Exceptions.printStackTrace(ex);
450: }
451:
452: for (SelectionRule rule : rules) {
453: if (!rule.appliesTo(info)) {
454: continue;
455: }
456:
457: //if (!HintsSettings.isEnabled(rule)) {
458: // continue;
459: //}
460:
461: rule.run(context, result);
462: }
463: }
464:
465: private void scan(Node node, AstPath path, CompilationInfo info,
466: Map<Integer, List<AstRule>> hints, int caretOffset,
467: List<Description> result) {
468: applyRules(node.nodeId, node, path, info, hints, caretOffset,
469: result);
470:
471: @SuppressWarnings(value="unchecked")
472: List<Node> list = node.childNodes();
473:
474: for (Node child : list) {
475: if (isCancelled()) {
476: return;
477: }
478:
479: path.descend(child);
480: scan(child, path, info, hints, caretOffset, result);
481: path.ascend();
482: }
483: }
484:
485: public void cancel() {
486: cancelled = true;
487: }
488:
489: private boolean isCancelled() {
490: return cancelled;
491: }
492:
493: /** For testing purposes only! */
494: public void setTestingHints(Map<Integer, List<AstRule>> testHints,
495: Map<Integer, List<AstRule>> testSuggestions,
496: Map<String, List<ErrorRule>> testErrors,
497: List<SelectionRule> testSelectionHints) {
498: this .testHints = testHints;
499: this .testSuggestions = testSuggestions;
500: this .testErrors = testErrors;
501: this .testSelectionHints = testSelectionHints;
502: }
503:
504: private static class FixWrapper implements EnhancedFix {
505: private org.netbeans.modules.ruby.hints.spi.Fix fix;
506: private String sortText;
507:
508: FixWrapper(org.netbeans.modules.ruby.hints.spi.Fix fix,
509: String sortText) {
510: this .fix = fix;
511: this .sortText = sortText;
512: }
513:
514: public String getText() {
515: return fix.getDescription();
516: }
517:
518: public ChangeInfo implement() throws Exception {
519: fix.implement();
520:
521: return null;
522: }
523:
524: public CharSequence getSortText() {
525: return sortText;
526: }
527: }
528: }
|