/** * Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.codesourcery.jasm16.ast; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import de.codesourcery.jasm16.compiler.GenericCompilationError; import de.codesourcery.jasm16.compiler.ICompilationError; import de.codesourcery.jasm16.compiler.ICompilationUnit; import de.codesourcery.jasm16.exceptions.EOFException; import de.codesourcery.jasm16.exceptions.ParseException; import de.codesourcery.jasm16.lexer.ILexer; import de.codesourcery.jasm16.lexer.IToken; import de.codesourcery.jasm16.lexer.TokenType; import de.codesourcery.jasm16.parser.IParseContext; import de.codesourcery.jasm16.parser.IParser.ParserOption; import de.codesourcery.jasm16.scanner.IScanner; import de.codesourcery.jasm16.utils.ITextRegion; import de.codesourcery.jasm16.utils.TextRegion; /** * Abstract base-class of all AST nodes. * * <p>AST nodes are created for one or more tokens in the input stream.</p> * <p>Each AST node keeps track of the source code location ({@link ITextRegion} it * was created from so editors etc. have an easy time associating source code * with the AST.</p> * * <p>Keeping track of the source code locations is slightly complicated * because not all tokens (e.g. whitespace,EOL) become part of the AST, so this * class actually uses two {@link ITextRegion} fields to keep track of the * source code range covered by the AST node (or it's children) and * the input that was actually traversed while this subtree was constructed.</p>. * * <p>Make sure you understand how {@link #getTextRegion()} , {@link #recalculateTextRegion(boolean)}, * {@link #setTextRegionIncludingAllTokens(ITextRegion)} and {@link #mergeWithAllTokensTextRegion(ASTNode)} * work.</p> * * <p>This class also implements the parse error recovery mechanism, check out {@link #parse(IParseContext)} to * see how it actually works.</p> * * @author tobias.gierke@code-sourcery.de */ public abstract class ASTNode { private static final Logger LOG = Logger.getLogger(ASTNode.class); /** * Default tokens to look for when trying to recover from * a parse error. * * @see IParseContext#setErrorRecoveryTokenTypes(TokenType[]) * @see ILexer#advanceTo(TokenType, boolean) */ public static final TokenType[] DEFAULT_ERROR_RECOVERY_TOKEN = new TokenType[]{TokenType.EOL, TokenType.SINGLE_LINE_COMMENT}; private ASTNode parent; private final List<ASTNode> children = new ArrayList<ASTNode>(); /** * This text range covers <b>all</b> tokens that were consumed while * parsing this node (including whitespace, EOL etc.). */ private ITextRegion textRegionIncludingAllTokens; /** * Cached value of {@link #textRegionIncludingAllTokens} plus {@link #getTextRegion()} of all * child nodes. */ private ITextRegion actualTextRegion; /** * Creates a new instance. * * <p>This instance will have no parent * and {@link #textRegionIncludingAllTokens} and {@link #actualTextRegion} * will be <code>null</code>.</p> */ public ASTNode() { } /** * Creates a new AST node for a given source code location. * * @param allTokensRegion text range covered by <b>this</b> node, never <code>null</code> */ protected ASTNode(ITextRegion allTokensRegion) { if (allTokensRegion == null) { throw new IllegalArgumentException("allTokensRegion must not be NULL."); } this.textRegionIncludingAllTokens = new TextRegion( allTokensRegion ); } /** * Returns the actual source code region covered by * this AST node (and it's child nodes). * * <p>Due to the way parsing works (recursive descent...), an AST node always * covers at least the text range that is covered by it's child nodes.</p> * * <p> * The actual source code region covered by <b>this</b> node is composed * of the actual source code regions of all child nodes (=invoking * {@link #getTextRegion()} on each child) <b>PLUS</b> * this node's <i>'all-tokens' text range</i> (see . * </p> * * @return * @see #mergeTextRegion(ITextRegion) * @see #mergeWithAllTokensTextRegion(ASTNode) */ public final ITextRegion getTextRegion() { if ( actualTextRegion == null ) { recalculateTextRegion(true); return actualTextRegion; } return actualTextRegion; } public final void adjustTextRegion(int offsetAdjust,int lengthAdjust) throws IllegalStateException { if ( hasChildren() ) { throw new IllegalStateException("adjustTextRegion() may only be called on LEAF nodes"); } final ITextRegion current = getTextRegion(); if ( current == null ) { throw new IllegalStateException("Node "+this+" has no text region assigned ?"); } if ( textRegionIncludingAllTokens != null ) { textRegionIncludingAllTokens = new TextRegion( current.getStartingOffset() + offsetAdjust , current.getLength() + lengthAdjust ); } actualTextRegion = new TextRegion( current.getStartingOffset() + offsetAdjust , current.getLength() + lengthAdjust ); recalculateTextRegion( true ); } /** * Merges the actual source code region covered by a node with * this node's {@link #textRegionIncludingAllTokens}. * * <p>This method is used during expression folding/evaluation to * preserve the text range covered by an AST node when an AST node * gets replaced with a newly created node representing calculated value. * </p> * <p>Not using this method during expression folding will cause the location * of all tokens that do not resemble actual AST nodes (read: almost all tokens) * to be permanently lost.</p> * * @param node * @see ITextRegion#merge(ITextRegion) */ protected final void mergeWithAllTokensTextRegion(ASTNode node) { mergeWithAllTokensTextRegion( node.getTextRegion() ); } /** * Merges the source code region with this node's {@link #textRegionIncludingAllTokens}. * * <p>This method is used during expression folding/evaluation to * preserve the text range covered by an AST node when an AST node * gets replaced with a newly created node representing calculated value. * </p> * <p>Not using this method during expression folding will cause the location * of all tokens that do not resemble actual AST nodes (read: almost all tokens) * to be permanently lost.</p> * * @param range * @see ITextRegion#merge(ITextRegion) */ protected final void mergeWithAllTokensTextRegion(List<? extends ITextRegion> range) { for ( ITextRegion r : range ) { mergeWithAllTokensTextRegion(r); } } /** * Merges the source code region with this node's {@link #textRegionIncludingAllTokens}. * * <p>This method is used during expression folding/evaluation to * preserve the text range covered by an AST node when an AST node * gets replaced with a newly created node representing calculated value. * </p> * <p>Not using this method during expression folding will cause the location * of all tokens that do not resemble actual AST nodes (read: almost all tokens) * to be permanently lost.</p> * * @param range * @see ITextRegion#merge(ITextRegion) */ protected final void mergeWithAllTokensTextRegion(ITextRegion range) { if ( this.textRegionIncludingAllTokens == null && this.actualTextRegion != null ) { this.textRegionIncludingAllTokens = this.actualTextRegion; } if ( this.textRegionIncludingAllTokens == null ) { this.textRegionIncludingAllTokens = new TextRegion( range ); } else { this.textRegionIncludingAllTokens.merge( range ); } if ( this.actualTextRegion != null ) { this.actualTextRegion = null; if ( getParent() != null ) { // maybe a parent node already called getTextRegion() on this child... getParent().recalculateTextRegion(true); } } } protected final void mergeTextRegion(ITextRegion range) { final int oldValue = TextRegion.hashCode( this.actualTextRegion ); if ( this.actualTextRegion == null && textRegionIncludingAllTokens != null) { this.actualTextRegion = new TextRegion( textRegionIncludingAllTokens ); } if ( this.actualTextRegion != null ) { this.actualTextRegion.merge( range ); } else { this.actualTextRegion = new TextRegion( range ); } if ( oldValue != TextRegion.hashCode( this.actualTextRegion ) && getParent() != null ) { getParent().mergeTextRegion( this.actualTextRegion ); } } protected void setTextRegionIncludingAllTokens( ITextRegion textRegion ) { this.textRegionIncludingAllTokens = new TextRegion( textRegion ); this.actualTextRegion = null; recalculateTextRegion(true); } private void recalculateTextRegion(boolean recalculateParents) { final int oldValue = TextRegion.hashCode( this.actualTextRegion ); ITextRegion range = textRegionIncludingAllTokens != null ? new TextRegion( textRegionIncludingAllTokens ) : null; for ( ASTNode child : this.children) { if ( range == null ) { range = new TextRegion( child.getTextRegion() ); } else { if ( child.getTextRegion() == null ) { throw new IllegalStateException("Child "+child+" has NULL text region?"); } range.merge( child.getTextRegion() ); } } this.actualTextRegion = range; if ( recalculateParents && oldValue != TextRegion.hashCode( this.actualTextRegion ) && getParent() != null ) { getParent().recalculateTextRegion(true); } } /** * Creates a copy of this AST node (and optionally all it's children recursively). * * @param shallow whether to only copy this node or also recursively clone * all child nodes as well. * * @return */ public ASTNode createCopy(boolean shallow) { ASTNode result = copySingleNode(); if ( actualTextRegion != null ) { result.actualTextRegion = new TextRegion( actualTextRegion ); } if ( textRegionIncludingAllTokens != null ) { result.textRegionIncludingAllTokens = new TextRegion( textRegionIncludingAllTokens ); } if ( ! shallow ) { for ( ASTNode child : children ) { final ASTNode copy = child.createCopy( shallow ); result.addChild( copy , null ); } } return result; } /** * Returns an <b>independent</b> copy of this node <b>without</b> * any of it's children. * * @return */ protected abstract ASTNode copySingleNode(); /** * Check this AST node or any of it's child nodes is of class * {@link UnparsedContentNode}. * * @return */ public final boolean hasErrors() { final boolean[] hasErrors = new boolean[] { false }; final ISimpleASTNodeVisitor<UnparsedContentNode> visitor = new ISimpleASTNodeVisitor<UnparsedContentNode>() { @Override public boolean visit(UnparsedContentNode node) { hasErrors[0] = true; return false; } }; ASTUtils.visitNodesByType( this , visitor , UnparsedContentNode.class ); return hasErrors[0]; } /** * Swap a direct child of this node with some other node. * * @param childToSwap * @param otherNode */ public final void swapChild( ASTNode childToSwap, ASTNode otherNode) { if ( childToSwap == null ) { throw new IllegalArgumentException("childToSwap must not be NULL"); } if ( otherNode == null ) { throw new IllegalArgumentException("otherNode must not be NULL"); } assertSupportsChildNodes(); final int idx = children.indexOf( childToSwap ); if ( idx == -1 ) { throw new IllegalArgumentException("Node "+childToSwap+" is not a child of "+this); } final ASTNode otherParent = otherNode.getParent(); if ( otherParent == null ) { throw new IllegalArgumentException("Node "+otherNode+" has no parent?"); } final int otherIdx = otherParent.indexOf( otherNode ); setChild( idx , otherNode ); otherParent.setChild( otherIdx , childToSwap ); recalculateTextRegion(true); otherParent.recalculateTextRegion( true ); } /** * Returns the index of a direct child. * * @param node * @return */ public final int indexOf(ASTNode node) { return children.indexOf( node ); } /** * Inserts a new child node at a specific position. * * @param index * @param newChild * @param context parse context or <code>null</code>. If the context is not <code>null</code> and * the node being added is <b>not</b> an instance of {@link UnparsedContentNode} , the parse * contexts error recovery flag ({@link IParseContext#isRecoveringFromParseError()}) will be reset. See {@link ASTNode#parse(IParseContext)} for * a detailed explanation on parser error recovery. * @return */ public final ASTNode insertChild(int index,ASTNode newChild,IParseContext context) { return insertChild(index,newChild,context,true); } /** * Inserts a new child node at a specific position. * * @param index * @param newChild * @param context parse context or <code>null</code>. If the context is not <code>null</code> and * the node being added is <b>not</b> an instance of {@link UnparsedContentNode} , the parse * contexts error recovery flag ({@link IParseContext#isRecoveringFromParseError()}) will be reset. See {@link ASTNode#parse(IParseContext)} for * @param mergeTextRegion whether this node's text region should be joined with the new node * a detailed explanation on parser error recovery. * @return */ public final ASTNode insertChild(int index,ASTNode newChild,IParseContext context,boolean mergeTextRegion) { try { return addChild( index , newChild , mergeTextRegion ); } finally { if ( context != null ) { context.setRecoveringFromParseError( false ); } } } /** * Replaces a child node at a specific position. * * @param index * @param newChild */ public final void setChild( int index, ASTNode newChild) { if ( newChild == null ) { throw new IllegalArgumentException("newChild must not be NULL"); } if ( index < 0 || index >= children.size() ) { throw new IndexOutOfBoundsException("Invalid index "+index+" ( must be >= 0 and < "+children.size()+")"); } assertSupportsChildNodes(); children.set( index , newChild ); newChild.setParent( this ); } /** * Returns the Nth child. * * @param index * @return * @throws IndexOutOfBoundsException if the index is either less than zero or larger than {@link #getChildCount()} -1 */ public final ASTNode child(int index) { if ( index < 0 || index >= children.size() ) { throw new IndexOutOfBoundsException("Invalid index "+index+" , node "+this+ " has only "+children.size()+" children"); } return children.get( index ); } /** * Adds child nodes to this node. * * @param nodes * @param context parse context or <code>null</code>. If the context is not <code>null</code> and * the node being added is <b>not</b> an instance of {@link UnparsedContentNode} , the parse * contexts error recovery flag ({@link IParseContext#isRecoveringFromParseError()}) will be reset. See {@link ASTNode#parse(IParseContext)} for * a detailed explanation on parser error recovery. */ public final void addChildren(Collection<? extends ASTNode> nodes,IParseContext context) { if (nodes == null) { throw new IllegalArgumentException("node must not be NULL."); } boolean recovering = context == null ? false : context.isRecoveringFromParseError(); try { for ( ASTNode node : nodes) { if ( recovering && !(node instanceof UnparsedContentNode) ) { recovering = false; } addChild( 0 , node ); } } finally { if ( context != null ) { context.setRecoveringFromParseError( recovering ); } } } /** * Returns whether this AST node supports having child nodes. * * <p>If a node does not support having child nodes, calling * and of the methods that add/change child nodes will trigger * an {@link UnsupportedOperationException}. * * @return */ public abstract boolean supportsChildNodes(); /** * Add a child node. * * @param node * @param context parse context or <code>null</code>. If the context is not <code>null</code> and * the node being added is <b>not</b> an instance of {@link UnparsedContentNode} , the parse * contexts error recovery flag ({@link IParseContext#isRecoveringFromParseError()}) will be reset. See {@link ASTNode#parse(IParseContext)} for * a detailed explanation on parser error recovery. * @return */ public final ASTNode addChild(ASTNode node,IParseContext context) { return addChild(node,context,true); } /** * Add a child node. * * @param node * @param context parse context or <code>null</code>. If the context is not <code>null</code> and * the node being added is <b>not</b> an instance of {@link UnparsedContentNode} , the parse * contexts error recovery flag ({@link IParseContext#isRecoveringFromParseError()}) will be reset. See {@link ASTNode#parse(IParseContext)} for * a detailed explanation on parser error recovery. * @param mergeTextRegion whether to merge the text region from the newly added child with the subtree * this node is in * @return */ public final ASTNode addChild(ASTNode node,IParseContext context,boolean mergeTextRegion) { try { return addChild( children.size() , node , mergeTextRegion ); } finally { if ( context != null && context.isRecoveringFromParseError() && !(node instanceof UnparsedContentNode) ) { context.setRecoveringFromParseError( false ); } } } /** * (INTERNAL USE ONLY , macro expansion). * * @param node */ public final void internalAddChild(ASTNode node) { addChild( children.size() , node , false ); } private final ASTNode addChild(int index , ASTNode node) { return addChild(index,node,true); } private final ASTNode addChild(int index , ASTNode node,boolean mergeTextRegion) { if (node == null) { throw new IllegalArgumentException("node must not be NULL."); } // all nodes must accept this if ( !(node instanceof UnparsedContentNode ) ) { assertSupportsChildNodes(); } if ( index == children.size() ) { this.children.add( node ); } else if ( index < 0 ) { throw new IndexOutOfBoundsException("Invalid child index "+index); } else if ( index < children.size() ) { this.children.add( index , node ); } else { throw new IndexOutOfBoundsException("Child index "+index+" is out of range, node "+this+" only has "+getChildCount()+" children."); } if ( mergeTextRegion ) { if ( node.textRegionIncludingAllTokens != null ) { if ( this.textRegionIncludingAllTokens == null ) { this.textRegionIncludingAllTokens = new TextRegion( node.textRegionIncludingAllTokens ); } else { this.textRegionIncludingAllTokens.merge( node.textRegionIncludingAllTokens ); } } mergeTextRegion( node.getTextRegion() ); } node.setParent( this ); return node; } protected final void assertSupportsChildNodes() { if ( ! supportsChildNodes() ) { throw new UnsupportedOperationException("Cannot add children to node "+this+" that does not support child nodes"); } } /** * Returns the number of direct children this node has. * * @return */ public final int getChildCount() { return children.size(); } /** * Returns whether this node has any children. * * @return */ public final boolean hasChildren() { return ! children.isEmpty(); } /** * Returns the child nodes of this node. * * @return */ public final List<ASTNode> getChildren() { return new ArrayList<ASTNode>( children ); } private final void setParent(ASTNode parent) { this.parent = parent; } /** * Returns the parent node of this node. * * @return parent or <code>null</code> if this node has no parent */ public final ASTNode getParent() { return parent; } /** * Parse source code (recursive decent parsing). * * <p> * This method delegates to {@link #parseInternal(IParseContext)} and * takes care of handling any <code>Exceptions</code> thrown by this * method appropriately. * </p> * <p> * The idiom used to continue parsing after encountering a parse error * is (see below for a detailed explanation): * <pre> * try { * context.mark(); * // setErrorRecoveryTokens( TOKENS ); * addChild( new SomeASTNode().parseInternal( context ); * } catch(Exception e) { * addCompilationErrorAndAdvanceParser( e , context ); * } finally { * context.clearMark(); * // setErrorRecoveryTokens( DEFAULT_TOKENS ); * } * // continue here regardless of parse error * </pre> * </p> * <p> * <h3>Parse error recovery</h3> * </p> * <p>Parse error recovery is tricky because it involves several different * parts of the application to interact correctly.</p> * <p><h4>Part 1 - The scanner</h4></p> * <p>The {@link IScanner} provides random access to the input stream using the {@link IScanner#setCurrentParseIndex(int)} method.</p> * <p><h4>Part 2 - The lexer</h4></p> * <p>The {@link ILexer} internally manages a stack of state information (current line number, line starting offset, current parse index,parsed tokens). * This state can be remembered/recalled using the {@link ILexer#mark()} , {@link ILexer#reset()} methods. The {@link ILexer#clearMark()} method * removes the last remembered state from the internal stack.</p> * <p><h4>Part 3 - {@link #parse(IParseContext)}</h4></p> * <p>Upon entry, this method remembers the lexer's state by calling {@link ILexer#mark()}. It then invokes {@link #parseInternal(IParseContext)} inside * a <code>try/finally</code> block. If the <code>parseInternal()</code> method fails with an exception, {@link #addCompilationErrorAndAdvanceParser(Exception, IParseContext)} is invoked. * The <code>finally</code> block of this method calls {@link ILexer#clearMark()} to remove the no longer needed lexer state information from the lexer's internal stack * and ensures that even if we saw a {@link OutOfMemoryError}, the lexer's internal stack does not grow infinitely.</p> * <p><h4>Part 4 - {@link #addCompilationErrorAndAdvanceParser(Exception, IParseContext)}</h4></p> * <p>This method first invokes {@link ILexer#reset()} to reset the lexer to the state it was when {@link #parse(IParseContext)} got called. It then * uses {@link ILexer#advanceTo(TokenType[], boolean)} to advance until a suitable token (see {@link #getParseRecoveryTokenTypes()} is found. * </p> * <p>All tokens skipped during advancing will combined into a {@link UnparsedContentNode} that is attached to <b>this</b> node.</p> * <p>If the parse context is <b>not</b> in recovery mode yet (see {@link IParseContext#isRecoveringFromParseError()} , an {@link ICompilationError} will be * added to the current compilation unit using {@link ICompilationUnit#addMarker(de.codesourcery.jasm16.compiler.IMarker)} and * the context will switch to error recovery mode by invoking {@link IParseContext#setRecoveringFromParseError(boolean)}}.</p> * <p>If the parse context was already in recovery mode when {@link #onError(Exception, IParseContext)} got invoked, <b>no</b> compilation error will * be added to the current compilation unit since we obviously haven't recovered from the last error yet.</p> * <p><h4>Part 5 - {@link ASTNode#addChild(ASTNode, IParseContext)} and friends</h4></p> * All {@link ASTNode} methods that actually add one or more new child nodes to a node will reset the parse' contexts error recovery flag when * the node being added is <b>not</b> an instance of {@link UnparsedContentNode}.</p> * * @param context * @return AST node parsed from the current parse position. Usually that * will be an instance of the class this method was invoked on but in case * of compilation errors this method may just return an {@link UnparsedContentNode} * so be <b>careful</b> when assuming the actual type returned by this method. */ public final ASTNode parse(IParseContext context) { context.mark(); try { ASTNode result = parseInternal( context ); return result; } catch(Exception e) { return addCompilationErrorAndAdvanceParser( e , context ); } finally { context.clearMark(); } } private final ICompilationError wrapException(Exception e, IParseContext context) { System.out.println("=== ASTNode#wrapException(): "+e.getMessage()); Throwable cause = e; while ( cause.getCause() != null ) { cause = cause.getCause(); } cause.printStackTrace(); final int errorOffset; final ITextRegion errorRange; if ( e instanceof ParseException ) { errorRange = ((ParseException) e).getTextRegion(); errorOffset = errorRange.getStartingOffset(); } else if ( e instanceof ICompilationError) { errorOffset = ((ICompilationError) e).getErrorOffset(); errorRange = ((ICompilationError) e).getLocation(); } else if ( e instanceof java.text.ParseException ) { errorOffset = ((java.text.ParseException) e).getErrorOffset(); errorRange = new TextRegion(errorOffset,0); } else if ( e instanceof EOFException) { errorOffset = ((EOFException) e).getErrorOffset(); errorRange = new TextRegion(errorOffset,0); } else { errorOffset = context.currentParseIndex(); errorRange = new TextRegion(errorOffset,0); } final ICompilationError result; if ( e instanceof ICompilationError) { result = (ICompilationError) e; } else { String msg=e.getMessage(); if ( StringUtils.isBlank( msg ) ) { msg = "< no error message >"; } result = new GenericCompilationError( msg , context.getCompilationUnit() , e ); result.setErrorOffset( errorOffset ); result.setLocation( errorRange ); } if ( result.getErrorOffset() != -1 ) { if ( result.getLineNumber() == -1 || result.getColumnNumber() == -1 || result.getLineStartOffset() == -1 ) { if ( result.getLineStartOffset() == -1 ) { result.setLineStartOffset( context.getCurrentLineStartOffset() ); } if (result.getLineNumber() == -1) { result.setLineNumber( context.getCurrentLineNumber() ); } if (result.getColumnNumber() == -1 ) { int column = result.getErrorOffset() - context.getCurrentLineStartOffset()+1; // columns start at 1 if ( column > 0 ) { result.setColumnNumber( column ); } else { LOG.warn("wrapException(): Error offset "+result.getErrorOffset()+" is not on current line "+context.getCurrentLineNumber()+", starting at "+ context.getCurrentLineStartOffset()); } } } if ( result.getLocation() == null && errorRange != null ) { result.setLocation( errorRange ); } } return result; } /** * Add compilation error and switch parser to recovery mode if it isn't already. * * See {@link #addCompilationErrorAndAdvanceParser(String, int, Exception, IParseContext)} for a description of how this method works. * * @param e * @param context * @return {@link UnparsedContentNode} that was added as a child to <b>this</b> node */ protected final ASTNode addCompilationErrorAndAdvanceParser(Exception e , IParseContext context) { return addCompilationErrorAndAdvanceParser( wrapException( e , context ) , context ); } protected final ASTNode addCompilationErrorAndAdvanceParser(Exception e , TokenType[] recoveryTokens, IParseContext context) { return addCompilationErrorAndAdvanceParser( wrapException( e , context ) , recoveryTokens , context ); } protected final ASTNode addCompilationErrorAndAdvanceParser(ICompilationError error, IParseContext context) { return addCompilationErrorAndAdvanceParser( error , DEFAULT_ERROR_RECOVERY_TOKEN , context ); } protected final ASTNode addCompilationErrorAndAdvanceParser(ICompilationError error, TokenType[] recoveryTokens, IParseContext context) { context.reset(); final List<IToken> tokens = context.advanceTo( recoveryTokens , false ); final UnparsedContentNode result = new UnparsedContentNode( error.getMessage() , error.getErrorOffset() , tokens ); if ( context.hasParserOption( ParserOption.DEBUG_MODE ) ) { LOG.error("addCompilationErrorAndAdvanceParser(): [in_parse_error_recovery: "+context.isRecoveringFromParseError() +"] error="+error,error.getCause() ); } addChild( result , context ); if ( ! context.isRecoveringFromParseError() ) { context.getCompilationUnit().addMarker( error ); /* * This flag will be reset when an ASTNode that is not a UnparsedContentNode is added to the AST * (because this indicates that the parser (at least temporarily) was able to re-synchronize again. */ context.setRecoveringFromParseError( true ); } return result; } /** * Method to be implemented by subclasses, does the actual recursive-descent parsing. * * <p> * Parse exceptions thrown by implementations will be attached to the * current compilation unit as {@link ICompilationError} instances ; the * parser will then switch to error recovery mode and advance to the next token that * has one of the token types returned by {@link #getParseRecoveryTokenTypes()}. * </p> * <p> * See {@link #parse(IParseContext)}} for a detailed description of the error recovery mechanism. * </p> * @param context * @return * @throws ParseException */ protected abstract ASTNode parseInternal(IParseContext context) throws ParseException; @Override public String toString() { final StringBuilder builder = new StringBuilder(); for (Iterator<ASTNode> it = children.iterator(); it.hasNext();) { ASTNode child = it.next(); builder.append( child.toString() ); if ( it.hasNext() ) { builder.append(" , "); } } return getClass().getSimpleName()+" { "+builder.toString()+" }"; } /** * Replace a direct child of this node with another one. * * @param child * @param newNode */ public final void replaceChild(ASTNode child , ASTNode newNode ) { if ( child == null ) { throw new IllegalArgumentException("child must not be NULL"); } assertSupportsChildNodes(); final int idx = children.indexOf( child ); if ( idx == -1 ) { throw new IllegalArgumentException("Node "+child+" is not a child of "+this); } setChild( idx , newNode ); recalculateTextRegion(true); } /** * Returns the path to the root node. * * @return path to the root node, first element is the root node * while the last element is THIS node. */ public final ASTNode[] getPathToRoot() { List<ASTNode> path = new ArrayList<ASTNode>(); ASTNode current = this; do { path.add( current ); current = current.getParent(); } while( current != null ); Collections.reverse( path ); return path.toArray( new ASTNode[ path.size() ] ); } /** * Returns all AST nodes <b>below</b> this AST node * that overlap with a specific {@link ITextRegion}. * * @param visible * @return */ public final List<ASTNode> getNodesInRange(ITextRegion visible) { final List<ASTNode> result = new ArrayList<ASTNode>(); for ( ASTNode child : children ) { if ( child.getTextRegion().overlaps( visible ) ) { result.add( child ); } } return result; } /** * Recursively discovers the AST node that starts closest * to a given source code offset. * * @param offset * @return source code node or <code>null</code> if neither * this node nor any of it's children cover the given offset */ public final ASTNode getNodeInRange(int offset) { final SearchResult result= internalGetNodeInRange( this , offset ,0 ); return result == null ? null : result.node; } protected static final class SearchResult { public final ASTNode node; public final int depth; private SearchResult(ASTNode node, int depth) { this.node = node; this.depth = depth; } public boolean isMoreSpecificThan(SearchResult other) { return this.node.getTextRegion().getLength() <= other.node.getTextRegion().getLength() && this.depth > other.depth; } @Override public String toString() { return "SearchResult[ "+node.getTextRegion()+" , depth="+depth+", node="+node; } } private static final SearchResult internalGetNodeInRange(ASTNode node, int offset,int currentDepth) { final ITextRegion region = node.getTextRegion(); if ( region == null || ! region.contains( offset ) ) { return null; } SearchResult candidate = new SearchResult(node,currentDepth); // special case: IncludeSourceFileNodes have at most one child and that's the AST // of the included source file...we do not want to search this one ! if ( ! ( node instanceof IncludeSourceFileNode) ) { for ( ASTNode child : node.getChildren() ) { SearchResult tmp = internalGetNodeInRange( child , offset , currentDepth +1 ); if ( tmp != null && tmp.isMoreSpecificThan( candidate ) ) { candidate = tmp; } } } return candidate; } /** * Removes all child nodes. * * <p>Note that this method does <b>not</b> alter the text regions associated with this node.</p> */ public final void removeAllChildNodes() { this.children.clear(); } /** * Returns whether this node has a parent node. * * @return * @see #getParent() */ public boolean hasParent() { return getParent() != null; } /** * Returns whether this node has no parent node. * * @return * @see #getParent() */ public boolean hasNoParent() { return getParent() == null; } /** * Starting from the current node and ascending the tree upwards,looks for * the closest {@link LabelNode} that defines a global label. * * @return */ protected final LabelNode getPreviousGlobalLabel() { final StatementNode stmt = getStatement(); if ( stmt != null && stmt.hasParent() ) { ASTNode stmtParent = stmt.getParent(); int index = stmtParent.indexOf( stmt ); for ( int i = 0 ; i <= index ; i++ ) { StatementNode tmp = (StatementNode) stmtParent.child(i); LabelNode labelNode = tmp.getLabelNode(); if ( labelNode != null && labelNode.getLabel().isGlobalSymbol() ) { return labelNode; } } } return null; } public final StatementNode getStatement() { if ( this instanceof StatementNode) { return (StatementNode) this; } return getParent() != null ? getParent().getStatement() : null; } }