blob: 0b01282a990f70a5153bae375fff7e388533fdc4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.core.dom;
import java.util.HashMap;
import org.eclipse.jdt.core.compiler.CharOperation;
import org.eclipse.jdt.core.compiler.InvalidInputException;
import org.eclipse.jdt.internal.compiler.parser.Scanner;
import org.eclipse.jdt.internal.compiler.parser.TerminalTokens;
/**
* Internal class for associating comments with AST nodes.
*
* @since 3.0
*/
class DefaultCommentMapper {
Comment[] comments;
Scanner scanner;
// extended nodes storage
int leadingPtr;
ASTNode[] leadingNodes;
int[][] leadingIndexes;
int trailingPtr, lastTrailingPtr;
ASTNode[] trailingNodes;
int[][] trailingIndexes;
static final int STORAGE_INCREMENT = 16;
/**
* @param table the given table of comments
*/
DefaultCommentMapper(Comment[] table) {
this.comments = table;
}
boolean hasSameTable(Comment[] table) {
return this.comments == table;
}
/**
* Get comment of the list which includes a given position
*
* @param position The position belonging to the looked up comment
* @return comment which includes the given position or null if none was found
*/
Comment getComment(int position) {
if (this.comments == null) {
return null;
}
int size = this.comments.length;
if (size == 0) {
return null;
}
int index = getCommentIndex(0, position, 0);
if (index<0) {
return null;
}
return this.comments[index];
}
/*
* Get the index of comment which contains given position.
* If there's no matching comment, then return depends on exact parameter:
* = 0: return -1
* < 0: return index of the comment before the given position
* > 0: return index of the comment after the given position
*/
private int getCommentIndex(int start, int position, int exact) {
if (position == 0) {
if (this.comments.length > 0 && this.comments[0].getStartPosition() == 0) {
return 0;
}
return -1;
}
int bottom = start, top = this.comments.length - 1;
int i = 0, index = -1;
Comment comment = null;
while (bottom <= top) {
i = (bottom + top) /2;
comment = this.comments[i];
int commentStart = comment.getStartPosition();
if (position < commentStart) {
top = i-1;
} else if (position >=(commentStart+comment.getLength())) {
bottom = i+1;
} else {
index = i;
break;
}
}
if (index<0 && exact!=0) {
comment = this.comments[i];
if (position < comment.getStartPosition()) {
return exact<0 ? i-1 : i;
} else {
return exact<0 ? i : i+1;
}
}
return index;
}
/**
* Return all leading comments of a given node.
* @param node
* @return an array of Comment or null if there's no leading comment
*/
Comment[] getLeadingComments(ASTNode node) {
if (this.leadingPtr >= 0) {
int[] range = null;
for (int i=0; range==null && i<=this.leadingPtr; i++) {
if (this.leadingNodes[i] == node) range = this.leadingIndexes[i];
}
if (range != null) {
int length = range[1]-range[0]+1;
Comment[] leadComments = new Comment[length];
System.arraycopy(this.comments, range[0], leadComments, 0, length);
return leadComments;
}
}
return null;
}
/**
* Return all trailing comments of a given node.
* @param node
* @return an array of Comment or null if there's no trailing comment
*/
Comment[] getTrailingComments(ASTNode node) {
if (this.trailingPtr >= 0) {
int[] range = null;
for (int i=0; range==null && i<=this.trailingPtr; i++) {
if (this.trailingNodes[i] == node) range = this.trailingIndexes[i];
}
if (range != null) {
int length = range[1]-range[0]+1;
Comment[] trailComments = new Comment[length];
System.arraycopy(this.comments, range[0], trailComments, 0, length);
return trailComments;
}
}
return null;
}
/**
* Returns the extended start position of the given node. Unlike
* {@link ASTNode#getStartPosition()} and {@link ASTNode#getLength()},
* the extended source range may include comments and whitespace
* immediately before or after the normal source range for the node.
*
* @param node the node
* @return the 0-based character index, or <code>-1</code>
* if no source position information is recorded for this node
* @see #getExtendedLength(ASTNode)
* @since 3.0
*/
public int getExtendedStartPosition(ASTNode node) {
if (this.leadingPtr >= 0) {
int[] range = null;
for (int i=0; range==null && i<=this.leadingPtr; i++) {
if (this.leadingNodes[i] == node) range = this.leadingIndexes[i];
}
if (range != null) {
return this.comments[range[0]].getStartPosition() ;
}
}
return node.getStartPosition();
}
/*
* Returns the extended end position of the given node.
*/
public int getExtendedEnd(ASTNode node) {
int end = node.getStartPosition() + node.getLength();
if (this.trailingPtr >= 0) {
int[] range = null;
for (int i=0; range==null && i<=this.trailingPtr; i++) {
if (this.trailingNodes[i] == node) range = this.trailingIndexes[i];
}
if (range != null) {
Comment lastComment = this.comments[range[1]];
end = lastComment.getStartPosition() + lastComment.getLength();
}
}
return end-1;
}
/**
* Returns the extended source length of the given node. Unlike
* {@link ASTNode#getStartPosition()} and {@link ASTNode#getLength()},
* the extended source range may include comments and whitespace
* immediately before or after the normal source range for the node.
*
* @param node the node
* @return a (possibly 0) length, or <code>0</code>
* if no source position information is recorded for this node
* @see #getExtendedStartPosition(ASTNode)
* @see #getExtendedEnd(ASTNode)
* @since 3.0
*/
public int getExtendedLength(ASTNode node) {
return getExtendedEnd(node) - getExtendedStartPosition(node) + 1;
}
/*
* Initialize leading and trailing comments tables in whole nodes hierarchy of a compilation
* unit.
* Scanner is necessary to scan between nodes and comments and verify if there's
* nothing else than white spaces.
*/
void initialize(CompilationUnit unit, Scanner sc) {
// Init array pointers
this.leadingPtr = -1;
this.trailingPtr = -1;
// Init comments
this.comments = unit.optionalCommentTable;
if (this.comments == null) {
return;
}
int size = this.comments.length;
if (size == 0) {
return;
}
// Init scanner and start ranges computing
this.scanner = sc;
this.scanner.tokenizeWhiteSpace = true;
// Start unit visit
DefaultASTVisitor commentVisitor = new CommentMapperVisitor();
unit.accept(commentVisitor);
// Reduce leading arrays if necessary
int leadingCount = this.leadingPtr + 1;
if (leadingCount > 0 && leadingCount < this.leadingIndexes.length) {
System.arraycopy(this.leadingNodes, 0, this.leadingNodes = new ASTNode[leadingCount], 0, leadingCount);
System.arraycopy(this.leadingIndexes, 0, this.leadingIndexes= new int[leadingCount][], 0, leadingCount);
}
// Reduce trailing arrays if necessary
if (this.trailingPtr >= 0) {
// remove last remaining unresolved nodes
while (this.trailingIndexes[this.trailingPtr][0] == -1) {
this.trailingPtr--;
if (this.trailingPtr < 0) {
this.trailingIndexes = null;
this.trailingNodes = null;
break;
}
}
// reduce array size
int trailingCount = this.trailingPtr + 1;
if (trailingCount > 0 && trailingCount < this.trailingIndexes.length) {
System.arraycopy(this.trailingNodes, 0, this.trailingNodes = new ASTNode[trailingCount], 0, trailingCount);
System.arraycopy(this.trailingIndexes, 0, this.trailingIndexes= new int[trailingCount][], 0, trailingCount);
}
}
}
/**
* Search and store node leading comments. Comments are searched in position range
* from previous extended position to node start position. If one or several comment are found,
* returns first comment start position, otherwise returns node start position.
*
* First look after first comment before node start position and return if none was found...
*
* When first comment was found before node, goes up in comment list until one of
* following condition becomes true:
* 1) comment end is before previous end
* 2) comment start and previous end is on the same line but not on same line of node start
* 3) there's other than white characters between current node and comment
* 4) there's more than 1 line between current node and comment
*
* If at least one potential comment has been found, then no token should be on
* on the same line before, so remove all comments which do not verify this assumption.
*
* If finally there is a subset of comments, then store start and end indexes
* in leading comments table.
*/
int storeLeadingComments(ASTNode node, int previousEnd) {
// Init extended position
int nodeStart = node.getStartPosition();
int extended = nodeStart;
// Get line of node start position
int previousEndLine = this.scanner.getLineNumber(previousEnd);
int nodeStartLine = this.scanner.getLineNumber(nodeStart);
// Find first comment index
int idx = getCommentIndex(0, nodeStart, -1);
if (idx == -1) {
return nodeStart;
}
// Look after potential comments
int startIdx = -1;
int endIdx = idx;
int previousStart = nodeStart;
while (idx >= 0 && previousStart >= previousEnd) {
// Verify for each comment that there's only white spaces between end and start of {following comment|node}
Comment comment = this.comments[idx];
int commentStart = comment.getStartPosition();
int end = commentStart+comment.getLength()-1;
int commentLine = this.scanner.getLineNumber(commentStart);
if (end <= previousEnd || (commentLine == previousEndLine && commentLine != nodeStartLine)) {
// stop search on condition 1) and 2)
break;
} else if ((end+1) < previousStart) { // may be equals => then no scan is necessary
this.scanner.resetTo(end+1, previousStart);
try {
int token = this.scanner.getNextToken();
if (token != TerminalTokens.TokenNameWHITESPACE || this.scanner.currentPosition != previousStart) {
// stop search on condition 3)
// if first comment fails, then there's no extended position in fact
if (idx == endIdx) {
return nodeStart;
}
break;
}
} catch (InvalidInputException e) {
// Should not happen, but return no extended position...
return nodeStart;
}
// verify that there's no more than one line between node/comments
char[] gap = this.scanner.getCurrentIdentifierSource();
int nbrLine = 0;
int pos = -1;
while ((pos=CharOperation.indexOf('\n', gap,pos+1)) >= 0) {
nbrLine++;
}
if (nbrLine > 1) {
// stop search on condition 4)
break;
}
}
// Store previous infos
previousStart = commentStart;
startIdx = idx--;
}
if (startIdx != -1) {
// Verify that there's no token on the same line before first leading comment
int commentStart = this.comments[startIdx].getStartPosition();
if (previousEnd < commentStart && previousEndLine != nodeStartLine) {
int lastTokenEnd = previousEnd;
this.scanner.resetTo(previousEnd, commentStart);
try {
while (this.scanner.currentPosition < commentStart) {
if (this.scanner.getNextToken() != TerminalTokens.TokenNameWHITESPACE) {
lastTokenEnd = this.scanner.getCurrentTokenEndPosition();
}
}
} catch (InvalidInputException e) {
// do nothing
}
int lastTokenLine = this.scanner.getLineNumber(lastTokenEnd);
int length = this.comments.length;
while (startIdx<length && lastTokenLine == this.scanner.getLineNumber(this.comments[startIdx].getStartPosition()) && nodeStartLine != lastTokenLine) {
startIdx++;
}
}
// Store leading comments indexes
if (startIdx <= endIdx) {
if (++this.leadingPtr == 0) {
this.leadingNodes = new ASTNode[STORAGE_INCREMENT];
this.leadingIndexes = new int[STORAGE_INCREMENT][];
} else if (this.leadingPtr == this.leadingNodes.length) {
int newLength = (this.leadingPtr*3/2)+STORAGE_INCREMENT;
System.arraycopy(this.leadingNodes, 0, this.leadingNodes = new ASTNode[newLength], 0, this.leadingPtr);
System.arraycopy(this.leadingIndexes, 0, this.leadingIndexes = new int[newLength][], 0, this.leadingPtr);
}
this.leadingNodes[this.leadingPtr] = node;
this.leadingIndexes[this.leadingPtr] = new int[] { startIdx, endIdx };
extended = this.comments[endIdx].getStartPosition();
}
}
return extended;
}
/**
* Search and store node trailing comments. Comments are searched in position range
* from node end position to specified next start. If one or several comment are found,
* returns last comment end position, otherwise returns node end position.
*
* First look after first comment after node end position and return if none was found...
*
* When first comment was found after node, goes down in comment list until one of
* following condition becomes true:
* 1) comment start is after next start
* 2) there's other than white characters between current node and comment
* 3) there's more than 1 line between current node and comment
*
* If at least one potential comment has been found, then all of them has to be separated
* from following node. So, remove all comments which do not verify this assumption.
* Note that this verification is not applicable on last node.
*
* If finally there is a subset of comments, then store start and end indexes
* in trailing comments table.
*/
int storeTrailingComments(ASTNode node, int nextStart, boolean lastChild) {
// Init extended position
int nodeEnd = node.getStartPosition()+node.getLength()-1;
if (nodeEnd == nextStart) {
// special case for last child of its parent
if (++this.trailingPtr == 0) {
this.trailingNodes = new ASTNode[STORAGE_INCREMENT];
this.trailingIndexes = new int[STORAGE_INCREMENT][];
this.lastTrailingPtr = -1;
} else if (this.trailingPtr == this.trailingNodes.length) {
int newLength = (this.trailingPtr*3/2)+STORAGE_INCREMENT;
System.arraycopy(this.trailingNodes, 0, this.trailingNodes = new ASTNode[newLength], 0, this.trailingPtr);
System.arraycopy(this.trailingIndexes, 0, this.trailingIndexes = new int[newLength][], 0, this.trailingPtr);
}
this.trailingNodes[this.trailingPtr] = node;
this.trailingIndexes[this.trailingPtr] = new int[] { -1, -1 };
return nodeEnd;
}
int extended = nodeEnd;
// Get line number
int nodeEndLine = this.scanner.getLineNumber(nodeEnd);
// Find comments range index
int idx = getCommentIndex(0, nodeEnd, 1);
if (idx == -1) {
return nodeEnd;
}
// Look after potential comments
int startIdx = idx;
int endIdx = -1;
int length = this.comments.length;
int commentStart = extended+1;
int previousEnd = nodeEnd+1;
int sameLineIdx = -1;
while (idx<length && commentStart < nextStart) {
// get comment and leave if next starting position has been reached
Comment comment = this.comments[idx];
commentStart = comment.getStartPosition();
// verify that there's nothing else than white spaces between node/comments
if (commentStart >= nextStart) {
// stop search on condition 1)
break;
} else if (previousEnd < commentStart) {
this.scanner.resetTo(previousEnd, commentStart);
try {
int token = this.scanner.getNextToken();
if (token != TerminalTokens.TokenNameWHITESPACE || this.scanner.currentPosition != commentStart) {
// stop search on condition 2)
// if first index fails, then there's no extended position in fact...
if (idx == startIdx) {
return nodeEnd;
}
// otherwise we get the last index of trailing comment => break
break;
}
} catch (InvalidInputException e) {
// Should not happen, but return no extended position...
return nodeEnd;
}
// verify that there's no more than one line between node/comments
char[] gap = this.scanner.getCurrentIdentifierSource();
int nbrLine = 0;
int pos = -1;
while ((pos=CharOperation.indexOf('\n', gap,pos+1)) >= 0) {
nbrLine++;
}
if (nbrLine > 1) {
// stop search on condition 3)
break;
}
}
// Store index if we're on the same line than node end
int commentLine = this.scanner.getLineNumber(commentStart);
if (commentLine == nodeEndLine) {
sameLineIdx = idx;
}
// Store previous infos
previousEnd = commentStart+comment.getLength();
endIdx = idx++;
}
if (endIdx != -1) {
// Verify that following node start is separated
if (!lastChild) {
int nextLine = this.scanner.getLineNumber(nextStart);
int previousLine = this.scanner.getLineNumber(previousEnd);
if((nextLine - previousLine) <= 1) {
if (sameLineIdx == -1) return nodeEnd;
endIdx = sameLineIdx;
}
}
// Store trailing comments indexes
if (++this.trailingPtr == 0) {
this.trailingNodes = new ASTNode[STORAGE_INCREMENT];
this.trailingIndexes = new int[STORAGE_INCREMENT][];
this.lastTrailingPtr = -1;
} else if (this.trailingPtr == this.trailingNodes.length) {
int newLength = (this.trailingPtr*3/2)+STORAGE_INCREMENT;
System.arraycopy(this.trailingNodes, 0, this.trailingNodes = new ASTNode[newLength], 0, this.trailingPtr);
System.arraycopy(this.trailingIndexes, 0, this.trailingIndexes = new int[newLength][], 0, this.trailingPtr);
}
this.trailingNodes[this.trailingPtr] = node;
int[] nodeRange = new int[] { startIdx, endIdx };
this.trailingIndexes[this.trailingPtr] = nodeRange;
// Compute new extended end
extended = this.comments[endIdx].getStartPosition()+this.comments[endIdx].getLength()-1;
// Look for children unresolved extended end
ASTNode previousNode = node;
int ptr = this.trailingPtr - 1; // children extended end were stored before
unresolved: while (ptr >= 0) {
int[] range = this.trailingIndexes[ptr];
if (range[0] != -1 || range[1] != -1) break; // there's no more unresolved nodes
ASTNode unresolved = this.trailingNodes[ptr];
if (previousNode != unresolved.getParent()) break; // we're no longer in node ancestor hierarchy
this.trailingIndexes[ptr] = nodeRange;
previousNode = unresolved;
ptr--; // get previous node
}
// Remove remaining unresolved nodes
if (ptr > this.lastTrailingPtr) {
int offset = ptr - this.lastTrailingPtr;
for (int i=ptr+1; i<=this.trailingPtr; i++) {
this.trailingNodes[i-offset] = this.trailingNodes[i];
this.trailingIndexes[i-offset] = this.trailingIndexes[i];
}
this.trailingPtr -= offset;
}
this.lastTrailingPtr = this.trailingPtr;
}
return extended;
}
class CommentMapperVisitor extends DefaultASTVisitor {
HashMap waitingSiblings = new HashMap(10);
protected boolean visitNode(ASTNode node) {
// Get default previous end
ASTNode parent = node.getParent();
int previousEnd = parent.getStartPosition();
// Look for sibling node
ASTNode sibling = (ASTNode) this.waitingSiblings.get(parent);
if (sibling != null) {
// Found one previous sibling, so compute its trailing comments using current node start position
try {
previousEnd = storeTrailingComments(sibling, node.getStartPosition(), false);
} catch (Exception ex) {
// Give up extended ranges at this level if unexpected exception happens...
}
}
// Compute leading comments for current node
try {
storeLeadingComments(node, previousEnd);
} catch (Exception ex) {
// Give up extended ranges at this level if unexpected exception happens...
}
// Store current node as waiting sibling for its parent
this.waitingSiblings.put(parent, node);
// We're always ok to visit sub-levels
return true;
}
protected void endVisitNode(ASTNode node) {
// Look if a child node is waiting for trailing comments computing
ASTNode sibling = (ASTNode) this.waitingSiblings.get(node);
if (sibling != null) {
try {
storeTrailingComments(sibling, node.getStartPosition()+node.getLength()-1, true);
} catch (Exception ex) {
// Give up extended ranges at this level if unexpected exception happens...
}
}
}
public boolean visit ( CompilationUnit node) {
// do nothing special, just go down in sub-levels
return true;
}
}
}