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: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041: package org.netbeans.modules.ruby;
042:
043: import org.netbeans.modules.gsf.api.ParserResult.AstTreeNode;
044: import java.io.IOException;
045: import java.io.Reader;
046: import java.io.StringReader;
047: import java.util.Iterator;
048: import java.util.List;
049: import javax.swing.text.BadLocationException;
050: import org.jruby.ast.Node;
051: import org.jruby.ast.RootNode;
052: import org.jruby.common.IRubyWarnings;
053: import org.jruby.lexer.yacc.ISourcePosition;
054: import org.jruby.lexer.yacc.LexerSource;
055: import org.jruby.lexer.yacc.SyntaxException;
056: import org.jruby.parser.DefaultRubyParser;
057: import org.jruby.parser.RubyParserConfiguration;
058: import org.jruby.parser.RubyParserResult;
059: import org.netbeans.modules.gsf.api.CompilationInfo;
060: import org.netbeans.modules.ruby.elements.Element;
061: import org.netbeans.modules.gsf.api.ElementHandle;
062: import org.netbeans.modules.gsf.api.Error;
063: import org.netbeans.modules.gsf.api.OffsetRange;
064: import org.netbeans.modules.gsf.api.ParseEvent;
065: import org.netbeans.modules.gsf.api.ParseListener;
066: import org.netbeans.modules.gsf.api.Parser;
067: import org.netbeans.modules.gsf.api.ParserFile;
068: import org.netbeans.modules.gsf.api.ParserResult;
069: import org.netbeans.modules.gsf.api.PositionManager;
070: import org.netbeans.modules.gsf.api.Severity;
071: import org.netbeans.modules.gsf.api.SourceFileReader;
072: import org.netbeans.modules.gsf.api.TranslatedSource;
073: import org.netbeans.modules.ruby.elements.AstElement;
074: import org.netbeans.modules.ruby.elements.RubyElement;
075: import org.netbeans.modules.gsf.spi.DefaultError;
076: import org.openide.util.Exceptions;
077: import org.openide.util.NbBundle;
078:
079: /**
080: * Wrapper around JRuby to parse a buffer into an AST.
081: *
082: * @todo Rename to RubyParser for symmetry with RubyLexer
083: * @todo Idea: If you get a syntax error on the last line, it's probably a missing
084: * "end" much earlier. Go back and look for a method inside a method, and the outer
085: * method is probably missing an end (can use indentation to look for this as well).
086: * Create a quickfix to insert it.
087: * @todo Only look for missing-end if there's an unexpected end
088: * @todo If you get a "class definition in method body" error, there's a missing
089: * end - prior to the class!
090: * @todo "syntax error, unexpected tRCURLY" means that I also have a missing end,
091: * but we encountered a } before we got to it. I need to be bracketing this stuff.
092: *
093: * @author Tor Norbye
094: */
095: public final class RubyParser implements Parser {
096: private final PositionManager positions = createPositionManager();
097:
098: /**
099: * Creates a new instance of RubyParser
100: */
101: public RubyParser() {
102: }
103:
104: private static String asString(CharSequence sequence) {
105: if (sequence instanceof String) {
106: return (String) sequence;
107: } else {
108: return sequence.toString();
109: }
110: }
111:
112: /** Parse the given set of files, and notify the parse listener for each transition
113: * (compilation results are attached to the events )
114: */
115: public void parseFiles(Parser.Job job) {
116: ParseListener listener = job.listener;
117: SourceFileReader reader = job.reader;
118:
119: for (ParserFile file : job.files) {
120: ParseEvent beginEvent = new ParseEvent(
121: ParseEvent.Kind.PARSE, file, null);
122: listener.started(beginEvent);
123:
124: ParserResult result = null;
125:
126: try {
127: CharSequence buffer = reader.read(file);
128: String source = asString(buffer);
129: int caretOffset = reader.getCaretOffset(file);
130: if (caretOffset != -1 && job.translatedSource != null) {
131: caretOffset = job.translatedSource
132: .getAstOffset(caretOffset);
133: }
134: Context context = new Context(file, listener, source,
135: caretOffset, job.translatedSource);
136: result = parseBuffer(context, Sanitize.NONE);
137: } catch (IOException ioe) {
138: listener.exception(ioe);
139: }
140:
141: ParseEvent doneEvent = new ParseEvent(
142: ParseEvent.Kind.PARSE, file, result);
143: listener.finished(doneEvent);
144: }
145: }
146:
147: protected PositionManager createPositionManager() {
148: return new RubyPositionManager();
149: }
150:
151: /**
152: * Try cleaning up the source buffer around the current offset to increase
153: * likelihood of parse success. Initially this method had a lot of
154: * logic to determine whether a parse was likely to fail (e.g. invoking
155: * the isEndMissing method from bracket completion etc.).
156: * However, I am now trying a parse with the real source first, and then
157: * only if that fails do I try parsing with sanitized source. Therefore,
158: * this method has to be less conservative in ripping out code since it
159: * will only be used when the regular source is failing.
160: */
161: private boolean sanitizeSource(Context context, Sanitize sanitizing) {
162:
163: if (sanitizing == Sanitize.MISSING_END) {
164: context.sanitizedSource = context.source + ";end";
165: int start = context.source.length();
166: context.sanitizedRange = new OffsetRange(start, start + 4);
167: context.sanitizedContents = "";
168: return true;
169: }
170:
171: int offset = context.caretOffset;
172:
173: // Let caretOffset represent the offset of the portion of the buffer we'll be operating on
174: if ((sanitizing == Sanitize.ERROR_DOT)
175: || (sanitizing == Sanitize.ERROR_LINE)) {
176: offset = context.errorOffset;
177: }
178:
179: // Don't attempt cleaning up the source if we don't have the buffer position we need
180: if (offset == -1) {
181: return false;
182: }
183:
184: // The user might be editing around the given caretOffset.
185: // See if it looks modified
186: // Insert an end statement? Insert a } marker?
187: String doc = context.source;
188: if (offset > doc.length()) {
189: return false;
190: }
191:
192: if (sanitizing == Sanitize.BLOCK_START) {
193: try {
194: int start = RubyUtils.getRowFirstNonWhite(doc, offset);
195: if (start != -1 && start + 2 < doc.length()
196: && doc.regionMatches(start, "if", 0, 2)) {
197: // TODO - check lexer
198: char c = 0;
199: if (start + 2 < doc.length()) {
200: c = doc.charAt(start + 2);
201: }
202: if (!Character.isLetter(c)) {
203: int removeStart = start;
204: int removeEnd = removeStart + 2;
205: StringBuilder sb = new StringBuilder(doc
206: .length());
207: sb.append(doc.substring(0, removeStart));
208: for (int i = removeStart; i < removeEnd; i++) {
209: sb.append(' ');
210: }
211: if (removeEnd < doc.length()) {
212: sb.append(doc.substring(removeEnd, doc
213: .length()));
214: }
215: assert sb.length() == doc.length();
216: context.sanitizedRange = new OffsetRange(
217: removeStart, removeEnd);
218: context.sanitizedSource = sb.toString();
219: context.sanitizedContents = doc.substring(
220: removeStart, removeEnd);
221: return true;
222: }
223: }
224:
225: return false;
226: } catch (BadLocationException ble) {
227: Exceptions.printStackTrace(ble);
228: return false;
229: }
230: }
231:
232: try {
233: // Sometimes the offset shows up on the next line
234: if (RubyUtils.isRowEmpty(doc, offset)
235: || RubyUtils.isRowWhite(doc, offset)) {
236: offset = RubyUtils.getRowStart(doc, offset) - 1;
237: if (offset < 0) {
238: offset = 0;
239: }
240: }
241:
242: if (!(RubyUtils.isRowEmpty(doc, offset) || RubyUtils
243: .isRowWhite(doc, offset))) {
244: if ((sanitizing == Sanitize.EDITED_LINE)
245: || (sanitizing == Sanitize.ERROR_LINE)) {
246: // See if I should try to remove the current line, since it has text on it.
247: int lineEnd = RubyUtils.getRowLastNonWhite(doc,
248: offset);
249:
250: if (lineEnd != -1) {
251: StringBuilder sb = new StringBuilder(doc
252: .length());
253: int lineStart = RubyUtils.getRowStart(doc,
254: offset);
255: int rest = lineStart + 1;
256:
257: sb.append(doc.substring(0, lineStart));
258: sb.append('#');
259:
260: if (rest < doc.length()) {
261: sb
262: .append(doc.substring(rest, doc
263: .length()));
264: }
265: assert sb.length() == doc.length();
266:
267: context.sanitizedRange = new OffsetRange(
268: lineStart, lineEnd);
269: context.sanitizedSource = sb.toString();
270: context.sanitizedContents = doc.substring(
271: lineStart, lineEnd);
272: return true;
273: }
274: } else {
275: assert sanitizing == Sanitize.ERROR_DOT
276: || sanitizing == Sanitize.EDITED_DOT;
277: // Try nuking dots/colons from this line
278: // See if I should try to remove the current line, since it has text on it.
279: int lineStart = RubyUtils.getRowStart(doc, offset);
280: int lineEnd = offset - 1;
281: while (lineEnd >= lineStart
282: && lineEnd < doc.length()) {
283: if (!Character
284: .isWhitespace(doc.charAt(lineEnd))) {
285: break;
286: }
287: lineEnd--;
288: }
289: if (lineEnd > lineStart) {
290: StringBuilder sb = new StringBuilder(doc
291: .length());
292: String line = doc.substring(lineStart,
293: lineEnd + 1);
294: int removeChars = 0;
295: int removeEnd = lineEnd + 1;
296:
297: if (line.endsWith(".") || line.endsWith("(")) { // NOI18N
298: removeChars = 1;
299: } else if (line.endsWith(",")) { // NOI18N removeChars = 1;
300: removeChars = 1;
301: } else if (line.endsWith(",:")) { // NOI18N
302: removeChars = 2;
303: } else if (line.endsWith(", :")) { // NOI18N
304: removeChars = 3;
305: } else if (line.endsWith(", ")) { // NOI18N
306: removeChars = 2;
307: } else if (line.endsWith("=> :")) { // NOI18N
308: removeChars = 4;
309: } else if (line.endsWith("=>:")) { // NOI18N
310: removeChars = 3;
311: } else if (line.endsWith("=>")) { // NOI18N
312: removeChars = 2;
313: } else if (line.endsWith("::")) { // NOI18N
314: removeChars = 2;
315: } else if (line.endsWith(":")) { // NOI18N
316: removeChars = 1;
317: } else if (line.endsWith("@@")) { // NOI18N
318: removeChars = 2;
319: } else if (line.endsWith("@")) { // NOI18N
320: removeChars = 1;
321: } else if (line.endsWith(",)")) { // NOI18N
322: // Handle lone comma in parameter list - e.g.
323: // type "foo(a," -> you end up with "foo(a,|)" which doesn't parse - but
324: // the line ends with ")", not "," !
325: // Just remove the comma
326: removeChars = 1;
327: removeEnd--;
328: } else if (line.endsWith(", )")) { // NOI18N
329: // Just remove the comma
330: removeChars = 1;
331: removeEnd -= 2;
332: }
333:
334: if (removeChars == 0) {
335: return false;
336: }
337:
338: int removeStart = removeEnd - removeChars;
339:
340: sb.append(doc.substring(0, removeStart));
341:
342: for (int i = 0; i < removeChars; i++) {
343: sb.append(' ');
344: }
345:
346: if (removeEnd < doc.length()) {
347: sb.append(doc.substring(removeEnd, doc
348: .length()));
349: }
350: assert sb.length() == doc.length();
351:
352: context.sanitizedRange = new OffsetRange(
353: removeStart, removeEnd);
354: context.sanitizedSource = sb.toString();
355: context.sanitizedContents = doc.substring(
356: removeStart, removeEnd);
357: return true;
358: }
359: }
360: }
361: } catch (BadLocationException ble) {
362: Exceptions.printStackTrace(ble);
363: }
364:
365: return false;
366: }
367:
368: @SuppressWarnings("fallthrough")
369: private RubyParseResult sanitize(final Context context,
370: final Sanitize sanitizing) {
371:
372: switch (sanitizing) {
373: case NEVER:
374: return createParseResult(context.file, null, null, null,
375: null);
376:
377: case NONE:
378:
379: // We've currently tried with no sanitization: try first level
380: // of sanitization - removing dots/colons at the edited offset.
381: // First try removing the dots or double colons around the failing position
382: if (context.caretOffset != -1) {
383: return parseBuffer(context, Sanitize.EDITED_DOT);
384: }
385:
386: // Fall through to try the next trick
387: case EDITED_DOT:
388:
389: // We've tried editing the caret location - now try editing the error location
390: // (Don't bother doing this if errorOffset==caretOffset since that would try the same
391: // source as EDITED_DOT which has no better chance of succeeding...)
392: if (context.errorOffset != -1
393: && context.errorOffset != context.caretOffset) {
394: return parseBuffer(context, Sanitize.ERROR_DOT);
395: }
396:
397: // Fall through to try the next trick
398: case ERROR_DOT:
399:
400: // We've tried removing dots - now try removing the whole line at the error position
401: if (context.caretOffset != -1) {
402: return parseBuffer(context, Sanitize.BLOCK_START);
403: }
404:
405: // Fall through to try the next trick
406: case BLOCK_START:
407:
408: // We've tried removing dots - now try removing the whole line at the error position
409: if (context.errorOffset != -1) {
410: return parseBuffer(context, Sanitize.ERROR_LINE);
411: }
412:
413: // Fall through to try the next trick
414: case ERROR_LINE:
415:
416: // Messing with the error line didn't work - we could try "around" the error line
417: // but I'm not attempting that now.
418: // Finally try removing the whole line around the user editing position
419: // (which could be far from where the error is showing up - but if you're typing
420: // say a new "def" statement in a class, this will show up as an error on a mismatched
421: // "end" statement rather than here
422: if (context.caretOffset != -1) {
423: return parseBuffer(context, Sanitize.EDITED_LINE);
424: }
425:
426: // Fall through to try the next trick
427: case EDITED_LINE:
428: return parseBuffer(context, Sanitize.MISSING_END);
429:
430: // Fall through for default handling
431: case MISSING_END:
432: default:
433: // We're out of tricks - just return the failed parse result
434: return createParseResult(context.file, null, null, null,
435: null);
436: }
437: }
438:
439: protected void notifyError(Context context, String key,
440: Severity severity, String description, String details,
441: int offset, Sanitize sanitizing) {
442: // Replace a common but unwieldy JRuby error message with a shorter one
443: if (description.startsWith("syntax error, expecting ")) { // NOI18N
444: int start = description.indexOf(" but found "); // NOI18N
445: assert start != -1;
446: start += 11;
447: int end = description.indexOf("instead", start); // NOI18N
448: assert end != -1;
449: String found = description.substring(start, end);
450: description = details = NbBundle.getMessage(
451: RubyParser.class, "UnexpectedError", found);
452: }
453:
454: // Initialize keys for errors needing it
455: if (key == null) {
456: key = description;
457: }
458:
459: Error error = new DefaultError(key, description, details,
460: context.file.getFileObject(), offset, offset, severity);
461: context.listener.error(error);
462:
463: if (sanitizing == Sanitize.NONE) {
464: context.errorOffset = offset;
465: }
466: }
467:
468: protected RubyParseResult parseBuffer(final Context context,
469: final Sanitize sanitizing) {
470: boolean sanitizedSource = false;
471: String source = context.source;
472: if (!((sanitizing == Sanitize.NONE) || (sanitizing == Sanitize.NEVER))) {
473: boolean ok = sanitizeSource(context, sanitizing);
474:
475: if (ok) {
476: assert context.sanitizedSource != null;
477: sanitizedSource = true;
478: source = context.sanitizedSource;
479: } else {
480: // Try next trick
481: return sanitize(context, sanitizing);
482: }
483: }
484:
485: Reader content = new StringReader(source);
486:
487: RubyParserResult result = null;
488:
489: final boolean ignoreErrors = sanitizedSource;
490:
491: try {
492: IRubyWarnings warnings = new IRubyWarnings() {
493: public void warn(ISourcePosition position,
494: String message) {
495: if (!ignoreErrors) {
496: notifyError(context, null, Severity.WARNING,
497: message, null, position
498: .getStartOffset(), sanitizing);
499: }
500: }
501:
502: public boolean isVerbose() {
503: return false;
504: }
505:
506: public void warn(String message) {
507: if (!ignoreErrors) {
508: notifyError(context, null, Severity.WARNING,
509: message, null, -1, sanitizing);
510: }
511: }
512:
513: public void warning(String message) {
514: if (!ignoreErrors) {
515: notifyError(context, null, Severity.WARNING,
516: message, null, -1, sanitizing);
517: }
518: }
519:
520: public void warning(ISourcePosition position,
521: String message) {
522: if (!ignoreErrors) {
523: notifyError(context, null, Severity.WARNING,
524: message, null, position
525: .getStartOffset(), sanitizing);
526: }
527: }
528: };
529:
530: //warnings.setFile(file);
531: DefaultRubyParser parser = new DefaultRubyParser();
532: parser.setWarnings(warnings);
533:
534: if (sanitizing == Sanitize.NONE) {
535: context.errorOffset = -1;
536: }
537:
538: String fileName = "";
539:
540: if ((context.file != null)
541: && (context.file.getFileObject() != null)) {
542: fileName = context.file.getFileObject().getNameExt();
543: }
544:
545: LexerSource lexerSource = new LexerSource(fileName,
546: content, 0, true);
547: RubyParserConfiguration configuration = new RubyParserConfiguration();
548: result = parser.parse(configuration, lexerSource);
549: } catch (SyntaxException e) {
550: int offset = e.getPosition().getStartOffset();
551:
552: // XXX should this be >, and = length?
553: if (offset >= source.length()) {
554: offset = source.length() - 1;
555:
556: if (offset < 0) {
557: offset = 0;
558: }
559: }
560:
561: if (!ignoreErrors) {
562: notifyError(context, null, Severity.ERROR, e
563: .getMessage(), e.getLocalizedMessage(), offset,
564: sanitizing);
565: }
566: }
567:
568: Node root = (result != null) ? result.getAST() : null;
569:
570: RootNode realRoot = null;
571:
572: if (root instanceof RootNode) {
573: // Quick workaround for now to avoid NPEs all over when
574: // code looks at RootNode, whose getPosition()==null.
575: // Its bodynode is what used to be returned as the root!
576: realRoot = (RootNode) root;
577: root = realRoot.getBodyNode();
578: }
579:
580: if (root != null) {
581: context.sanitized = sanitizing;
582: AstNodeAdapter ast = new AstNodeAdapter(null, root);
583: RubyParseResult r = createParseResult(context.file, ast,
584: root, realRoot, result);
585: r.setSanitized(context.sanitized, context.sanitizedRange,
586: context.sanitizedContents);
587: r.setSource(source);
588: return r;
589: } else {
590: return sanitize(context, sanitizing);
591: }
592: }
593:
594: protected RubyParseResult createParseResult(ParserFile file,
595: AstTreeNode ast, Node root, RootNode realRoot,
596: RubyParserResult jrubyResult) {
597: return new RubyParseResult(this , file, ast, root, realRoot,
598: jrubyResult);
599: }
600:
601: public PositionManager getPositionManager() {
602: return positions;
603: }
604:
605: @SuppressWarnings("unchecked")
606: public static RubyElement resolveHandle(CompilationInfo info,
607: ElementHandle handle) {
608: if (handle instanceof AstElement) {
609: AstElement element = (AstElement) handle;
610: CompilationInfo oldInfo = element.getInfo();
611: if (oldInfo == info) {
612: return element;
613: }
614: Node oldNode = element.getNode();
615: Node oldRoot = AstUtilities.getRoot(oldInfo);
616:
617: Node newRoot = AstUtilities.getRoot(info);
618: if (newRoot == null) {
619: return null;
620: }
621:
622: // Find newNode
623: Node newNode = find(oldRoot, oldNode, newRoot);
624:
625: if (newNode != null) {
626: AstElement co = AstElement.create(info, newNode);
627:
628: return co;
629: }
630: } else if (handle instanceof RubyElement) {
631: return (RubyElement) handle;
632: }
633:
634: return null;
635: }
636:
637: private static Node find(Node oldRoot, Node oldObject, Node newRoot) {
638: // Walk down the tree to locate oldObject, and in the process, pick the same child for newRoot
639: @SuppressWarnings("unchecked")
640: List<? extends Node> oldChildren = oldRoot.childNodes();
641: @SuppressWarnings("unchecked")
642: List<? extends Node> newChildren = newRoot.childNodes();
643: Iterator<? extends Node> itOld = oldChildren.iterator();
644: Iterator<? extends Node> itNew = newChildren.iterator();
645:
646: while (itOld.hasNext()) {
647: if (!itNew.hasNext()) {
648: return null; // No match - the trees have changed structure
649: }
650:
651: Node o = itOld.next();
652: Node n = itNew.next();
653:
654: if (o == oldObject) {
655: // Found it!
656: return n;
657: }
658:
659: // Recurse
660: Node match = find(o, oldObject, n);
661:
662: if (match != null) {
663: return match;
664: }
665: }
666:
667: if (itNew.hasNext()) {
668: return null; // No match - the trees have changed structure
669: }
670:
671: return null;
672: }
673:
674: /** Attempts to sanitize the input buffer */
675: public static enum Sanitize {
676: /** Only parse the current file accurately, don't try heuristics */
677: NEVER,
678: /** Perform no sanitization */
679: NONE,
680: /** Try to remove the trailing . or :: at the caret line */
681: EDITED_DOT,
682: /** Try to remove the trailing . or :: at the error position, or the prior
683: * line, or the caret line */
684: ERROR_DOT,
685: /** Try to remove the initial "if" or "unless" on the block
686: * in case it's not terminated
687: */
688: BLOCK_START,
689: /** Try to cut out the error line */
690: ERROR_LINE,
691: /** Try to cut out the current edited line, if known */
692: EDITED_LINE,
693: /** Attempt to add an "end" to the end of the buffer to make it compile */
694: MISSING_END,
695: }
696:
697: /** Parsing context */
698: public static class Context {
699: private final ParserFile file;
700: private final ParseListener listener;
701: private int errorOffset;
702: private String source;
703: private String sanitizedSource;
704: private OffsetRange sanitizedRange = OffsetRange.NONE;
705: private String sanitizedContents;
706: private int caretOffset;
707: private Sanitize sanitized = Sanitize.NONE;
708: private TranslatedSource translatedSource;
709:
710: public Context(ParserFile parserFile, ParseListener listener,
711: String source, int caretOffset,
712: TranslatedSource translatedSource) {
713: this .file = parserFile;
714: this .listener = listener;
715: this .source = source;
716: this .caretOffset = caretOffset;
717: this .translatedSource = translatedSource;
718: }
719:
720: @Override
721: public String toString() {
722: return "RubyParser.Context(" + file.toString() + ")"; // NOI18N
723: }
724:
725: public OffsetRange getSanitizedRange() {
726: return sanitizedRange;
727: }
728:
729: public Sanitize getSanitized() {
730: return sanitized;
731: }
732:
733: public String getSanitizedSource() {
734: return sanitizedSource;
735: }
736:
737: public int getErrorOffset() {
738: return errorOffset;
739: }
740: }
741: }
|