/******************************************************************************* * Copyright (c) 2009 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 * Zend Technologies *******************************************************************************/ package org2.eclipse.php.internal.core.ast.nodes; import java.io.IOException; import java.io.Reader; import java.util.Collection; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org2.eclipse.php.internal.core.ast.scanner.AstLexer; import org2.eclipse.php.internal.core.ast.visitor.AbstractVisitor; import org2.eclipse.php.internal.core.ast.visitor.ApplyAll; /** * Internal class for associating comments with AST nodes. * * @since 3.0 */ public class DefaultCommentMapper { Comment[] comments; AstLexer scanner; // extended nodes storage int leadingPtr; ASTNode[] leadingNodes; long[] leadingIndexes; int trailingPtr, lastTrailingPtr; ASTNode[] trailingNodes; long[] trailingIndexes; private IDocument document; 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].getStart() == 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 - bottom) / 2; comment = this.comments[i]; int commentStart = comment.getStart(); 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.getStart()) { return exact < 0 ? i - 1 : i; } else { return exact < 0 ? i : i + 1; } } return index; } /** * Returns the extended start position of the given node. Unlike * {@link ASTNode#getStart()} 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) { long range = -1; for (int i = 0; range < 0 && i <= this.leadingPtr; i++) { if (this.leadingNodes[i] == node) range = this.leadingIndexes[i]; } if (range >= 0) { return this.comments[(int) (range >> 32)].getStart(); } } return node.getStart(); } /* * Search the line number corresponding to a specific position between the * given line range (inclusive) * * @param position int * * @param lineRange size-2 int[] * * @return int */ public final int getLineNumber(int position, int[] lineRange) { try { return this.document.getLineOfOffset(position); } catch (BadLocationException e) { throw new IllegalArgumentException( "getLineNumber() in DefaultCommentMapper with " + position); //$NON-NLS-1$ } } /* * Returns the extended end position of the given node. */ public int getExtendedEnd(ASTNode node) { int end = node.getStart() + node.getLength(); if (this.trailingPtr >= 0) { long range = -1; for (int i = 0; range < 0 && i <= this.trailingPtr; i++) { if (this.trailingNodes[i] == node) range = this.trailingIndexes[i]; } if (range >= 0) { Comment lastComment = this.comments[(int) range]; end = lastComment.getStart() + lastComment.getLength(); } } return end - 1; } /** * Returns the extended source length of the given node. Unlike * {@link ASTNode#getStart()} 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; } /** * Return index of first leading comment of a given node. * * @param node * @return index of first leading comment or -1 if node has no leading * comment */ int firstLeadingCommentIndex(ASTNode node) { if (this.leadingPtr >= 0) { for (int i = 0; i <= this.leadingPtr; i++) { if (this.leadingNodes[i] == node) { return (int) (this.leadingIndexes[i] >> 32); } } } return -1; } /** * Return index of last trailing comment of a given node. * * @param node * @return index of last trailing comment or -1 if node has no trailing * comment */ int lastTrailingCommentIndex(ASTNode node) { if (this.trailingPtr >= 0) { for (int i = 0; i <= this.trailingPtr; i++) { if (this.trailingNodes[i] == node) { return (int) this.trailingIndexes[i]; } } } return -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(Program unit, AstLexer sc, IDocument document) { if (document == null) { throw new IllegalArgumentException(); } this.document = document; // Init array pointers this.leadingPtr = -1; this.trailingPtr = -1; // Init comments final Collection<Comment> commentsCollection = unit.comments(); if (this.comments == null) { return; } this.comments = (Comment[]) commentsCollection .toArray(new Comment[commentsCollection.size()]); int size = this.comments.length; if (size == 0) { return; } // Init scanner and start ranges computing this.scanner = sc; // TODO : this.scanner.tokenizeWhiteSpace = true; // Start unit visit AbstractVisitor 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 long[leadingCount], 0, leadingCount); } // Reduce trailing arrays if necessary if (this.trailingPtr >= 0) { // remove last remaining unresolved nodes while (this.trailingIndexes[this.trailingPtr] == -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 long[trailingCount], 0, trailingCount); } } // Release scanner as it's only used during unit visit this.scanner = null; } /** * 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. * <p> * Starts to search for first comment before node start position and return * if none was found... * </p> * <p> * When first comment is found before node, goes up in comment list until * one of following conditions becomes true: * <ol> * <li>comment end is before previous end</li> * <li>comment start and previous end is on the same line but not on same * line of node start</li> * <li>there's other than white characters between current node and comment</li> * <li>TODO : there's more than 1 line between current node and comment</li> * </ol> * If some comment have been found, then no token should be on on the same * line before, so remove all comments which do not verify this assumption. * </p> * <p> * If finally there's leading still comments, then stores indexes of the * first and last one in leading comments table. */ int storeLeadingComments(ASTNode node, int previousEnd, int[] parentLineRange) { // Init extended position int nodeStart = node.getStart(); int extended = nodeStart; // Get line of node start position int previousEndLine = getLineNumber(previousEnd, parentLineRange); int nodeStartLine = getLineNumber(nodeStart, parentLineRange); // 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.getStart(); int end = commentStart + comment.getLength() - 1; int commentLine = getLineNumber(commentStart, parentLineRange); 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 try { resetTo(end + 1, previousStart); this.scanner.next_token(); String token = this.scanner.yytext(); if (token != null && token.trim().length() > 0) { // stop search on condition 3) // if first comment fails, then there's no extended // position in fact if (idx == endIdx) { return nodeStart; } break; } } catch (Exception e) { // Should not happen, but return no extended position... assert false; return nodeStart; } } // Store previous infos previousStart = commentStart; startIdx = idx--; } if (startIdx != -1) { // Store leading comments indexes if (startIdx <= endIdx) { if (++this.leadingPtr == 0) { this.leadingNodes = new ASTNode[STORAGE_INCREMENT]; this.leadingIndexes = new long[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 long[newLength], 0, this.leadingPtr); } this.leadingNodes[this.leadingPtr] = node; this.leadingIndexes[this.leadingPtr] = (((long) startIdx) << 32) + endIdx; extended = this.comments[endIdx].getStart(); } } return extended; } private void resetTo(int begin, int end) throws IOException { if (scanner == null) { throw new IllegalArgumentException( "null at resetTo(int begin, int end)"); //$NON-NLS-1$ } this.scanner.yyreset(new IntervalDocumentReader(this.document, begin, end)); this.scanner.setInScriptingState(); } /** * 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. * <p> * Starts to search for first comment after node end position and return if * none was found... * </p> * <p> * When first comment is found after node, goes down in comment list until * one of following conditions becomes true: * <ol> * <li>comment start is after next start</li> * <li>there's other than white characters between current node and comment</li> * <li>TODO there's more than 1 line between current node and comment</li> * </ol> * If at least potential comments have 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. * </p> * <p> * If finally there's still trailing comments, then stores indexes of the * first and last one in trailing comments table. */ int storeTrailingComments(ASTNode node, int nextStart, boolean lastChild, int[] parentLineRange) { // Init extended position int nodeEnd = node.getStart() + 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 long[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 long[newLength], 0, this.trailingPtr); } this.trailingNodes[this.trailingPtr] = node; this.trailingIndexes[this.trailingPtr] = -1; return nodeEnd; } int extended = nodeEnd; // Get line number int nodeEndLine = getLineNumber(nodeEnd, parentLineRange); // 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.getStart(); // 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) { try { resetTo(previousEnd, commentStart); this.scanner.next_token(); String token = this.scanner.yytext(); if (token != null && token.trim().length() > 0) { // 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 (Exception e) { // Should not happen, but return no extended position... assert false; return nodeEnd; } } // Store index if we're on the same line than node end int commentLine = getLineNumber(commentStart, parentLineRange); 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 = getLineNumber(nextStart, parentLineRange); int previousLine = getLineNumber(previousEnd, parentLineRange); 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 long[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 long[newLength], 0, this.trailingPtr); } this.trailingNodes[this.trailingPtr] = node; long nodeRange = (((long) startIdx) << 32) + endIdx; this.trailingIndexes[this.trailingPtr] = nodeRange; // Compute new extended end extended = this.comments[endIdx].getStart() + 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 while (ptr >= 0) { long range = this.trailingIndexes[ptr]; if (range != -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 ApplyAll { ASTNode topSiblingParent = null; ASTNode[] siblings = new ASTNode[10]; int[][] parentLineRange = new int[10][]; int siblingPtr = -1; protected boolean apply(ASTNode node) { // Get default previous end ASTNode parent = node.getParent(); int previousEnd = parent.getStart(); // Look for sibling node ASTNode sibling = parent == this.topSiblingParent ? (ASTNode) this.siblings[this.siblingPtr] : null; if (sibling != null) { // Found one previous sibling, so compute its trailing comments // using current node start position try { previousEnd = storeTrailingComments(sibling, node .getStart(), false, this.parentLineRange[this.siblingPtr]); } catch (Exception ex) { // Give up extended ranges at this level if unexpected // exception happens... } } // Stop visit for malformed node (see bug // https://bugs.eclipse.org/bugs/show_bug.cgi?id=84049) if (node.getType() == ASTNode.AST_ERROR) { return false; } // Compute leading comments for current node int[] previousLineRange = this.siblingPtr > -1 ? this.parentLineRange[this.siblingPtr] : new int[] { 1, DefaultCommentMapper.this.document .getNumberOfLines() }; try { storeLeadingComments(node, previousEnd, previousLineRange); } catch (Exception ex) { // Give up extended ranges at this level if unexpected exception // happens... } // Store current node as waiting sibling for its parent if (this.topSiblingParent != parent) { if (this.siblings.length == ++this.siblingPtr) { System.arraycopy(this.siblings, 0, this.siblings = new ASTNode[this.siblingPtr * 2], 0, this.siblingPtr); System.arraycopy( this.parentLineRange, 0, this.parentLineRange = new int[this.siblingPtr * 2][], 0, this.siblingPtr); } if (this.topSiblingParent == null) { // node is a CompilationUnit this.parentLineRange[this.siblingPtr] = previousLineRange; } else { int parentStart = parent.getStart(); int firstLine = getLineNumber(parentStart, previousLineRange); int lastLine = getLineNumber(parentStart + parent.getLength() - 1, previousLineRange); if (this.parentLineRange[this.siblingPtr] == null) { this.parentLineRange[this.siblingPtr] = new int[] { firstLine, lastLine }; } else { int[] lineRange = this.parentLineRange[this.siblingPtr]; lineRange[0] = firstLine; lineRange[1] = lastLine; } } this.topSiblingParent = parent; } this.siblings[this.siblingPtr] = node; // We're always ok to visit sub-levels return true; } public void endVisitNode(ASTNode node) { // Look if a child node is waiting for trailing comments computing ASTNode sibling = this.topSiblingParent == node ? (ASTNode) this.siblings[this.siblingPtr] : null; if (sibling != null) { try { storeTrailingComments(sibling, node.getStart() + node.getLength() - 1, true, this.parentLineRange[this.siblingPtr]); } catch (Exception ex) { // Give up extended ranges at this level if unexpected // exception happens... } } // Remove sibling if needed if (this.topSiblingParent != null /* not a CompilationUnit */ && this.topSiblingParent == node) { this.siblingPtr--; this.topSiblingParent = node.getParent(); } } public boolean visit(Program node) { return true; } public boolean visit(Comment node) { // don't visit comments return false; } } /** * Returns a stream that represents the document * * @param StructuredDocument * @param start * @param length */ public static class IntervalDocumentReader extends Reader { private static final String BAD_LOCATION_ERROR = "Bad location error "; //$NON-NLS-1$ final private IDocument parent; private int startPhpRegion; final private int endPhpRegion; public IntervalDocumentReader(final IDocument parent, final int startPhpRegion, final int endPhpRegion) { this.parent = parent; this.startPhpRegion = startPhpRegion; this.endPhpRegion = endPhpRegion; } @Override public int read() throws IOException { try { return startPhpRegion < endPhpRegion ? parent .getChar(startPhpRegion++) : -1; } catch (BadLocationException e) { throw new IOException(BAD_LOCATION_ERROR + startPhpRegion); } } @Override public int read(char[] b, int off, int len) throws IOException { if (b == null) { throw new NullPointerException(); } else if ((off < 0) || (off > b.length) || (len < 0) || ((off + len) > b.length) || ((off + len) < 0)) { throw new IndexOutOfBoundsException(); } else if (len == 0) { return 0; } int c = read(); if (c == -1) { return -1; } b[off] = (char) c; int i = 1; for (; i < len; i++) { c = read(); if (c == -1) { break; } if (b != null) { b[off + i] = (char) c; } } return i; } @Override public void close() throws IOException { } } }