/**
* 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;
}
}