/** * Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved. * Licensed under the terms of the Eclipse Public License (EPL). * Please see the license.txt included with this distribution for details. * Any modifications to this file must keep this entire header intact. */ /* * Author: atotic * Created: Jul 25, 2003 */ package org.python.pydev.parser; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.internal.resources.ResourceException; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IResource; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IAdaptable; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.IRegion; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IFileEditorInput; import org.eclipse.ui.texteditor.MarkerUtilities; import org.python.pydev.core.ExtensionHelper; import org.python.pydev.core.IGrammarVersionProvider; import org.python.pydev.core.IPyEdit; import org.python.pydev.core.IPythonNature; import org.python.pydev.core.MisconfigurationException; import org.python.pydev.core.Tuple3; import org.python.pydev.core.log.Log; import org.python.pydev.core.parser.ChangedParserInfoForObservers; import org.python.pydev.core.parser.ErrorParserInfoForObservers; import org.python.pydev.core.parser.IParserObserver; import org.python.pydev.core.parser.IParserObserver2; import org.python.pydev.core.parser.IParserObserver3; import org.python.pydev.core.parser.IPyParser; import org.python.pydev.parser.fastparser.FastParser; import org.python.pydev.parser.grammar24.PythonGrammar24; import org.python.pydev.parser.grammar25.PythonGrammar25; import org.python.pydev.parser.grammar26.PythonGrammar26; import org.python.pydev.parser.grammar27.PythonGrammar27; import org.python.pydev.parser.grammar30.PythonGrammar30; import org.python.pydev.parser.jython.FastCharStream; import org.python.pydev.parser.jython.ParseException; import org.python.pydev.parser.jython.SimpleNode; import org.python.pydev.parser.jython.Token; import org.python.pydev.parser.jython.TokenMgrError; import org.python.pydev.parser.jython.ast.Module; import org.python.pydev.parser.jython.ast.stmtType; import com.aptana.shared_core.callbacks.ICallback; import com.aptana.shared_core.io.FileUtils; import com.aptana.shared_core.structure.Tuple; /** * PyParser uses org.python.parser to parse the document (lexical analysis) It * is attached to PyEdit (a view), and it listens to document changes On every * document change, the syntax tree is regenerated The reparsing of the document * is done on a ParsingThread * * Clients that need to know when new parse tree has been generated should * register as parseListeners. */ @SuppressWarnings("restriction") public class PyParser implements IPyParser { /** * Just for tests: show whenever we're not able to parse some file. */ public static boolean DEBUG_SHOW_PARSE_ERRORS = false; /** * just for tests, when we don't have any editor */ public static boolean ACCEPT_NULL_INPUT_EDITOR = false; /** * Defines whether we should use the fast stream or not */ public static boolean USE_FAST_STREAM = true; /** * this is the document we should parse */ private volatile IDocument document; /** * ast for the last successful parsing */ private SimpleNode root = null; /** * listens to changes in the document */ private IDocumentListener documentListener; /** * listeners that get notified of successful or unsuccessful parser achievements */ private ArrayList<IParserObserver> parserListeners; /** * used to enable tracing in the grammar */ public static boolean ENABLE_TRACING = false; /** * this is the object that will keep parser schedules for us (and will call us for doing parsing when requested) */ private ParserScheduler scheduler; /** * indicates we should do analysis only on doc save */ private boolean useAnalysisOnlyOnDocSave; /** * This is the version of the grammar that should be used for this parser */ private final IGrammarVersionProvider grammarVersionProvider; /** * Identifies whether this parser is disposed. */ private volatile boolean disposed = false; public static String getGrammarVersionStr(int grammarVersion) { if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_2_4) { return "grammar: Python 2.4"; } else if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_2_5) { return "grammar: Python 2.5"; } else if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_2_6) { return "grammar: Python 2.6"; } else if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_2_7) { return "grammar: Python 2.7"; } else if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_3_0) { return "grammar: Python 3.0"; } else if (grammarVersion == IGrammarVersionProvider.GRAMMAR_PYTHON_VERSION_CYTHON) { return "grammar: Cython"; } else { return "grammar: unrecognized: " + grammarVersion; } } public int getGrammarVersion() throws MisconfigurationException { return grammarVersionProvider.getGrammarVersion(); } /** * Should only be called for testing. Does not register as a thread. */ public PyParser(IGrammarVersionProvider grammarVersionProvider) { if (grammarVersionProvider == null) { grammarVersionProvider = new IGrammarVersionProvider() { public int getGrammarVersion() { return IPythonNature.LATEST_GRAMMAR_VERSION; } }; } this.grammarVersionProvider = grammarVersionProvider; parserListeners = new ArrayList<IParserObserver>(); scheduler = new ParserScheduler(this); documentListener = new IDocumentListener() { public void documentChanged(DocumentEvent event) { if (useAnalysisOnlyOnDocSave) { //if we're doing analysis only on doc change, the parser will not give any changes //to the scheduler, so, we won't have any parse events to respond to return; } String text = event.getText(); boolean parseNow = true; if (event == null || text == null) { parseNow = false; } if (parseNow) { if (text.indexOf("\n") == -1 && text.indexOf("\r") == -1) { parseNow = false; } } if (!parseNow) { // carriage return in changed text means parse now, anything // else means parse later scheduler.parseLater(); } else { scheduler.parseNow(); } } public void documentAboutToBeChanged(DocumentEvent event) { } }; } /** * Ok, create the parser for an editor * * @param editorView */ public PyParser(IPyEdit editorView) { this(getGrammarProviderFromEdit(editorView)); } /** * @param editorView this is the editor that we're getting in the parser * @return a provider signaling the grammar to be used for the parser. * @throws MisconfigurationException */ private static IGrammarVersionProvider getGrammarProviderFromEdit(IPyEdit editorView) { return editorView.getGrammarVersionProvider(); } /** * should be called when the editor is disposed */ public void dispose() { this.disposed = true; this.scheduler.dispose(); // remove the listeners if (document != null) { document.removeDocumentListener(documentListener); } synchronized (parserListeners) { parserListeners.clear(); } } public SimpleNode getRoot() { return root; } public void notifySaved() { //force parse on save forceReparse(); } /** * @return false if we asked a reparse and it will not be scheduled because a reparse is already in action. */ public boolean forceReparse(Object... argsToReparse) { if (disposed) { return true; //reparse didn't happen, but no matter what happens, it won't happen anyways } return scheduler.parseNow(true, argsToReparse); } /** * This is the input from the editor that we're using in the parse */ private IEditorInput input; public void setDocument(IDocument document, IEditorInput input) { setDocument(document, true, input); } public synchronized void setDocument(IDocument doc, boolean addToScheduler, IEditorInput input) { this.input = input; // Cleans up old listeners if (this.document != null) { this.document.removeDocumentListener(documentListener); } // Set up new listener this.document = doc; if (doc == null) { Log.log("No document in PyParser::setDocument?"); return; } doc.addDocumentListener(documentListener); if (addToScheduler) { // Reparse document on the initial set (force it) scheduler.parseNow(true); } } // ---------------------------------------------------------------------------- listeners /** stock listener implementation */ public void addParseListener(IParserObserver listener) { Assert.isNotNull(listener); synchronized (parserListeners) { if (!parserListeners.contains(listener)) { parserListeners.add(listener); } } } /** stock listener implementation */ public void removeParseListener(IParserObserver listener) { Assert.isNotNull(listener); synchronized (parserListeners) { parserListeners.remove(listener); } } // ---------------------------------------------------------------------------- notifications /** * stock listener implementation event is fired whenever we get a new root * @param original */ @SuppressWarnings("unchecked") protected void fireParserChanged(ChangedParserInfoForObservers info) { this.root = (SimpleNode) info.root; List<IParserObserver> temp; synchronized (parserListeners) { temp = new ArrayList<IParserObserver>(parserListeners); } for (IParserObserver l : temp) { try { //work on a copy (because listeners may want to remove themselves and we cannot afford concurrent modifications here) if (l instanceof IParserObserver3) { ((IParserObserver3) l).parserChanged(info); } else if (l instanceof IParserObserver2) { ((IParserObserver2) l).parserChanged(info.root, info.file, info.doc, info.argsToReparse); } else { l.parserChanged(info.root, info.file, info.doc); } } catch (Exception e) { Log.log(e); } } List<IParserObserver> participants = ExtensionHelper.getParticipants(ExtensionHelper.PYDEV_PARSER_OBSERVER); for (IParserObserver observer : participants) { try { if (observer instanceof IParserObserver3) { ((IParserObserver3) observer).parserChanged(info); } else if (observer instanceof IParserObserver2) { ((IParserObserver2) observer).parserChanged(info.root, info.file, info.doc, info.argsToReparse); } else { observer.parserChanged(info.root, info.file, info.doc); } } catch (Exception e) { Log.log(e); } } } /** * stock listener implementation event is fired when parse fails * @param original */ @SuppressWarnings("unchecked") protected void fireParserError(ErrorParserInfoForObservers info) { List<IParserObserver> temp; synchronized (parserListeners) { temp = new ArrayList<IParserObserver>(parserListeners); } for (IParserObserver l : temp) {//work on a copy (because listeners may want to remove themselves and we cannot afford concurrent modifications here) if (l instanceof IParserObserver3) { ((IParserObserver3) l).parserError(info); } else if (l instanceof IParserObserver2) { ((IParserObserver2) l).parserError(info.error, info.file, info.doc, info.argsToReparse); } else { l.parserError(info.error, info.file, info.doc); } } List<IParserObserver> participants = ExtensionHelper.getParticipants(ExtensionHelper.PYDEV_PARSER_OBSERVER); for (IParserObserver observer : participants) { if (observer instanceof IParserObserver3) { ((IParserObserver3) observer).parserError(info); } else if (observer instanceof IParserObserver2) { ((IParserObserver2) observer).parserError(info.error, info.file, info.doc, info.argsToReparse); } else { observer.parserError(info.error, info.file, info.doc); } } } // ---------------------------------------------------------------------------- parsing /** * Parses the document, generates error annotations * * @param argsToReparse: will be passed to fireParserError / fireParserChanged so that the IParserObserver2 * can check it. This is useful when the reparse was done with some specific thing in mind, so that its requestor * can pass some specific thing to the parser observers * * @return a tuple with the SimpleNode root(if parsed) and the error (if any). * if we are able to recover from a reparse, we have both, the root and the error. */ public Tuple<SimpleNode, Throwable> reparseDocument(Object... argsToReparse) { //get the document ast and error in object int version; try { version = grammarVersionProvider.getGrammarVersion(); } catch (MisconfigurationException e1) { //Ok, we cannot get it... let's put on the default version = IGrammarVersionProvider.LATEST_GRAMMAR_VERSION; } long documentTime = System.currentTimeMillis(); Tuple<SimpleNode, Throwable> obj = reparseDocument(new ParserInfo(document, version, true)); IFile original = null; IAdaptable adaptable = null; if (input == null) { return obj; } original = (input instanceof IFileEditorInput) ? ((IFileEditorInput) input).getFile() : null; if (original != null) { adaptable = original; } else { //probably an external file, may have some location provider mechanism //it may be org.eclipse.ui.internal.editors.text.JavaFileEditorInput adaptable = input; } //delete the markers if (original != null) { try { deleteErrorMarkers(original); } catch (ResourceException e) { //ok, if it is a resource exception, it may have happened because the resource does not exist anymore //so, there is no need to log this failure if (original.exists()) { Log.log(e); } } catch (CoreException e) { Log.log(e); } } else if (adaptable == null) { //ok, we have nothing... maybe we are in tests... if (!PyParser.ACCEPT_NULL_INPUT_EDITOR) { throw new RuntimeException("Null input editor received in parser!"); } } //end delete the markers if (disposed) { //if it was disposed in this time, don't fire any notification nor return anything valid. return new Tuple<SimpleNode, Throwable>(null, null); } if (obj.o1 != null) { //Ok, reparse successful, lets erase the markers that are in the editor we just parsed //Note: we may get the ast even if errors happen (and we'll notify in that case too). ChangedParserInfoForObservers info = new ChangedParserInfoForObservers(obj.o1, adaptable, document, documentTime, argsToReparse); fireParserChanged(info); } if (obj.o2 instanceof ParseException || obj.o2 instanceof TokenMgrError) { ErrorParserInfoForObservers info = new ErrorParserInfoForObservers(obj.o2, adaptable, document, argsToReparse); fireParserError(info); } return obj; } /** * This function will remove the markers related to errors. * @param resource the file that should have the markers removed * @throws CoreException */ public static void deleteErrorMarkers(IResource resource) throws CoreException { IMarker[] markers = resource.findMarkers(IMarker.PROBLEM, false, IResource.DEPTH_ZERO); if (markers.length > 0) { resource.deleteMarkers(IMarker.PROBLEM, false, IResource.DEPTH_ZERO); } } //static methods that can be used to get the ast (and error if any) -------------------------------------- public final static class ParserInfo { public IDocument document; /** * A set with the lines that were changed when trying to make the document parseable */ public final Set<Integer> linesChanged = new HashSet<Integer>(); /** * This is the version of the grammar to be used * @see IPythonNature.GRAMMAR_XXX constants */ public final int grammarVersion; /** * The module name of the contents parsed (may be null) */ public final String moduleName; /** * The file that's been parsed (may be null) */ public final File file; /** * Whether we should generate the tree as a parse result or we're just interested in errors. */ public final boolean generateTree; /** * @param grammarVersion: see IPythonNature.GRAMMAR_XXX constants */ public ParserInfo(IDocument document, int grammarVersion) { this(document, grammarVersion, null, null, true); } public ParserInfo(IDocument document, IGrammarVersionProvider nature) throws MisconfigurationException { this(document, nature.getGrammarVersion()); } public ParserInfo(IDocument document, IGrammarVersionProvider nature, String moduleName, File file) throws MisconfigurationException { this(document, nature.getGrammarVersion(), moduleName, file, true); } public ParserInfo(IDocument document, int grammarVersion, String name, File f, boolean generateTree) { this.document = document; this.grammarVersion = grammarVersion; this.moduleName = name; this.file = f; this.generateTree = generateTree; } public ParserInfo(IDocument document, IGrammarVersionProvider grammarProvider, boolean generateTree) throws MisconfigurationException { this(document, grammarProvider.getGrammarVersion(), null, null, generateTree); } public ParserInfo(IDocument document, int grammarVersion, boolean generateTree) { this(document, grammarVersion, null, null, generateTree); } public String toString() { StringBuffer buf = new StringBuffer(); buf.append("ParserInfo ["); buf.append("file:"); buf.append(file); buf.append("\nmoduleName:"); buf.append(moduleName); if (!generateTree) { buf.append(" NOT GENERATING TREE"); } buf.append("]"); return buf.toString(); } } /** * This list of callbacks is mostly used for testing, so that we can check what's been parsed. */ public final static List<ICallback<Object, Tuple3<SimpleNode, Throwable, ParserInfo>>> successfulParseListeners = new ArrayList<ICallback<Object, Tuple3<SimpleNode, Throwable, ParserInfo>>>(); /** * Create the char array to parse based on the initial document and our parser limitations. */ private static char[] createCharArrayToParse(String startDoc) { int length = startDoc.length(); int skipAtStart = 0; if (startDoc.startsWith(FileUtils.BOM_UTF8)) { skipAtStart = FileUtils.BOM_UTF8.length(); } else if (startDoc.startsWith(FileUtils.BOM_UNICODE)) { skipAtStart = FileUtils.BOM_UNICODE.length(); } int addAtEnd = 0; if (!startDoc.endsWith("\n") && !startDoc.endsWith("\r")) { addAtEnd = 1; } char[] charArray = new char[length - skipAtStart + addAtEnd]; startDoc.getChars(skipAtStart, length, charArray, 0); if (addAtEnd > 0) { charArray[charArray.length - 1] = '\n'; } return charArray; } /** * Actually creates the grammar. * @param generateTree whether we should generate the AST or not. */ private static IGrammar createGrammar(boolean generateTree, int grammarVersion, char[] charArray) { IGrammar grammar; FastCharStream in = new FastCharStream(charArray); switch (grammarVersion) { case IPythonNature.GRAMMAR_PYTHON_VERSION_2_4: grammar = new PythonGrammar24(generateTree, in); break; case IPythonNature.GRAMMAR_PYTHON_VERSION_2_5: grammar = new PythonGrammar25(generateTree, in); break; case IPythonNature.GRAMMAR_PYTHON_VERSION_2_6: grammar = new PythonGrammar26(generateTree, in); break; case IPythonNature.GRAMMAR_PYTHON_VERSION_2_7: grammar = new PythonGrammar27(generateTree, in); break; case IPythonNature.GRAMMAR_PYTHON_VERSION_3_0: grammar = new PythonGrammar30(generateTree, in); break; //case CYTHON: not treated here (only in reparseDocument). default: throw new RuntimeException("The grammar specified for parsing is not valid: " + grammarVersion); } if (ENABLE_TRACING) { //grammar has to be generated with debugging info for this to make a difference grammar.enable_tracing(); } return grammar; } /** * Note: this method should generally not be needed. Use reparseDocument on most situation (this * is mostly for tests or profilings). */ public static Tuple<SimpleNode, IGrammar> reparseDocumentInternal(IDocument doc, boolean generateTree, int grammarVersion) throws ParseException { char[] charArray = createCharArrayToParse(doc.get()); IGrammar grammar = createGrammar(generateTree, grammarVersion, charArray); return new Tuple<SimpleNode, IGrammar>(grammar.file_input(), grammar); // parses the file } /** * @return a tuple with the SimpleNode root(if parsed) and the error (if any). * if we are able to recover from a reparse, we have both, the root and the error. */ public static Tuple<SimpleNode, Throwable> reparseDocument(ParserInfo info) { if (info.grammarVersion == IPythonNature.GRAMMAR_PYTHON_VERSION_CYTHON) { IDocument doc = info.document; return createCythonAst(doc); } // create a stream with document's data String startDoc = info.document.get(); if (startDoc.trim().length() == 0) { //If empty, don't bother to parse! return new Tuple<SimpleNode, Throwable>(new Module(new stmtType[0]), null); } char[] charArray = createCharArrayToParse(startDoc); startDoc = null; //it can be garbage-collected now. Tuple<SimpleNode, Throwable> returnVar = new Tuple<SimpleNode, Throwable>(null, null); IGrammar grammar = null; try { grammar = createGrammar(info.generateTree, info.grammarVersion, charArray); SimpleNode newRoot = grammar.file_input(); // parses the file returnVar.o1 = newRoot; //only notify successful parses if (successfulParseListeners.size() > 0) { Tuple3<SimpleNode, Throwable, ParserInfo> param = new Tuple3<SimpleNode, Throwable, ParserInfo>( returnVar.o1, returnVar.o2, info); for (ICallback<Object, Tuple3<SimpleNode, Throwable, ParserInfo>> callback : successfulParseListeners) { callback.call(param); } } returnVar.o2 = grammar.getErrorOnParsing(); } catch (Throwable e) { //ok, some error happened when trying the parse... let's go and clear the local info before doing //another parse. if (DEBUG_SHOW_PARSE_ERRORS) { e.printStackTrace(); } //If the grammar was not created, the problem wasn't in the parsing... so, let's just rethrow the error if (grammar == null) { throw new RuntimeException(e); } //We have to change it for the 1st error we got (the one in catch is the last one). Throwable errorOnParsing = grammar.getErrorOnParsing(); if (errorOnParsing != null) { e = errorOnParsing; } else if (DEBUG_SHOW_PARSE_ERRORS) { System.out.println("Unhandled error"); e.printStackTrace(); } grammar = null; if (e instanceof ParseException || e instanceof TokenMgrError) { returnVar = new Tuple<SimpleNode, Throwable>(null, e); } else if (e.getClass().getName().indexOf("LookaheadSuccess") != -1) { //don't log this kind of error... } else { Log.log(e); } } if (DEBUG_SHOW_PARSE_ERRORS) { if (returnVar.o1 == null) { System.out.println("Unable to parse " + info); } } // System.out.println("Output grammar: "+returnVar); return returnVar; } public static Tuple<SimpleNode, Throwable> createCythonAst(IDocument doc) { List<stmtType> classesAndFunctions = FastParser.parseCython(doc); return new Tuple<SimpleNode, Throwable>(new Module(classesAndFunctions.toArray(new stmtType[classesAndFunctions .size()])), null); } public void resetTimeoutPreferences(boolean useAnalysisOnlyOnDocSave) { this.useAnalysisOnlyOnDocSave = useAnalysisOnlyOnDocSave; } public List<IParserObserver> getObservers() { synchronized (parserListeners) { return new ArrayList<IParserObserver>(this.parserListeners); } } /** * Adds the error markers for some error that was found in the parsing process. * * @param error the error find while parsing the document * @param resource the resource that should have the error added * @param doc the document with the resource contents * @return the error description (or null) * * @throws BadLocationException * @throws CoreException */ public static ErrorDescription createParserErrorMarkers(Throwable error, IAdaptable resource, IDocument doc) throws BadLocationException, CoreException { ErrorDescription errDesc; if (resource == null) { return null; } IResource fileAdapter = (IResource) resource.getAdapter(IResource.class); if (fileAdapter == null) { return null; } errDesc = createErrorDesc(error, doc); Map<String, Object> map = new HashMap<String, Object>(); map.put(IMarker.MESSAGE, errDesc.message); map.put(IMarker.SEVERITY, IMarker.SEVERITY_ERROR); map.put(IMarker.LINE_NUMBER, errDesc.errorLine); map.put(IMarker.CHAR_START, errDesc.errorStart); map.put(IMarker.CHAR_END, errDesc.errorEnd); map.put(IMarker.TRANSIENT, true); MarkerUtilities.createMarker(fileAdapter, map, IMarker.PROBLEM); return errDesc; } /** * Creates the error description for a given error in the parse. */ private static ErrorDescription createErrorDesc(Throwable error, IDocument doc) throws BadLocationException { int errorStart = -1; int errorEnd = -1; int errorLine = -1; String message = null; if (error instanceof ParseException) { ParseException parseErr = (ParseException) error; // Figure out where the error is in the document, and create a // marker for it if (parseErr.currentToken == null) { IRegion endLine = doc.getLineInformationOfOffset(doc.getLength()); errorStart = endLine.getOffset(); errorEnd = endLine.getOffset() + endLine.getLength(); } else { Token errorToken = parseErr.currentToken.next != null ? parseErr.currentToken.next : parseErr.currentToken; IRegion startLine = doc.getLineInformation(getDocPosFromAstPos(errorToken.beginLine)); IRegion endLine; if (errorToken.endLine == 0) { endLine = startLine; } else { endLine = doc.getLineInformation(getDocPosFromAstPos(errorToken.endLine)); } errorStart = startLine.getOffset() + getDocPosFromAstPos(errorToken.beginColumn); errorEnd = endLine.getOffset() + errorToken.endColumn; } message = parseErr.getMessage(); } else if (error instanceof TokenMgrError) { TokenMgrError tokenErr = (TokenMgrError) error; IRegion startLine = doc.getLineInformation(tokenErr.errorLine - 1); errorStart = startLine.getOffset(); errorEnd = startLine.getOffset() + tokenErr.errorColumn; message = tokenErr.getMessage(); } else { Log.log("Error, expecting ParseException or TokenMgrError. Received: " + error); return new ErrorDescription(null, 0, 0, 0); } errorLine = doc.getLineOfOffset(errorStart); // map.put(IMarker.LOCATION, "Whassup?"); this is the location field // in task manager if (message != null) { // prettyprint message = message.replaceAll("\\r\\n", " "); message = message.replaceAll("\\r", " "); message = message.replaceAll("\\n", " "); } return new ErrorDescription(message, errorLine, errorStart, errorEnd); } /** * The ast position starts at 1 and the document starts at 0 (but it could be that we had nothing valid * and received an invalid position, so, we must treat that). */ private static int getDocPosFromAstPos(int astPos) { if (astPos > 0) { astPos--; } return astPos; } }