--- /dev/null
+/*******************************************************************************
+ * Copyright (c) 2000, 2004 IBM Corporation and others.
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Common Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/cpl-v10.html
+ *
+ * Contributors:
+ * IBM Corporation - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.internal.compiler.parser;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.core.compiler.CharOperation;
+import org.eclipse.jdt.core.compiler.InvalidInputException;
+import org.eclipse.jdt.internal.compiler.classfmt.ClassFileConstants;
+
+/**
+ * Parser specialized for decoding javadoc comments
+ */
+public abstract class AbstractCommentParser {
+
+ // recognized tags
+ public static final char[] TAG_DEPRECATED = "deprecated".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_PARAM = "param".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_RETURN = "return".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_THROWS = "throws".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_EXCEPTION = "exception".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_SEE = "see".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_LINK = "link".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_LINKPLAIN = "linkplain".toCharArray(); //$NON-NLS-1$
+ public static final char[] TAG_INHERITDOC = "inheritDoc".toCharArray(); //$NON-NLS-1$
+
+ // tags expected positions
+ public final static int ORDERED_TAGS_NUMBER = 3;
+ public final static int PARAM_TAG_EXPECTED_ORDER = 0;
+ public final static int THROWS_TAG_EXPECTED_ORDER = 1;
+ public final static int SEE_TAG_EXPECTED_ORDER = 2;
+
+ // Kind of comment parser
+ public final static int COMPIL_PARSER = 0x00000001;
+ public final static int DOM_PARSER = 0x00000002;
+
+ // Public fields
+ public Scanner scanner;
+ public boolean checkDocComment = false;
+
+ // Protected fields
+ protected boolean inherited, deprecated;
+ protected char[] source;
+ protected int index, endComment, lineEnd;
+ protected int tokenPreviousPosition, lastIdentifierEndPosition, starPosition;
+ protected int textStart, memberStart;
+ protected int tagSourceStart, tagSourceEnd;
+ protected int inlineTagStart;
+ protected Parser sourceParser;
+ protected Object returnStatement;
+ protected boolean lineStarted = false, inlineTagStarted = false;
+ protected int kind;
+ protected int[] lineEnds;
+
+ // Private fields
+ private int currentTokenType = -1;
+
+ // Line pointers
+ private int linePtr, lastLinePtr;
+
+ // Identifier stack
+ protected int identifierPtr;
+ protected char[][] identifierStack;
+ protected int identifierLengthPtr;
+ protected int[] identifierLengthStack;
+ protected long[] identifierPositionStack;
+ // Ast stack
+ protected static int AstStackIncrement = 10;
+ protected int astPtr;
+ protected Object[] astStack;
+ protected int astLengthPtr;
+ protected int[] astLengthStack;
+
+ protected AbstractCommentParser(Parser sourceParser) {
+ this.sourceParser = sourceParser;
+ this.scanner = new Scanner(false, false, false, ClassFileConstants.JDK1_3, null, null, true/*taskCaseSensitive*/);
+ this.identifierStack = new char[20][];
+ this.identifierPositionStack = new long[20];
+ this.identifierLengthStack = new int[10];
+ this.astStack = new Object[30];
+ this.astLengthStack = new int[20];
+ }
+
+ /* (non-Javadoc)
+ * Returns true if tag @deprecated is present in javadoc comment.
+ *
+ * If javadoc checking is enabled, will also construct an Javadoc node, which will be stored into Parser.javadoc
+ * slot for being consumed later on.
+ */
+ protected boolean parseComment(int javadocStart, int javadocEnd) {
+
+ boolean validComment = true;
+ try {
+ // Init scanner position
+ this.scanner.resetTo(javadocStart, javadocEnd);
+ this.endComment = javadocEnd;
+ this.index = javadocStart;
+ readChar(); // starting '/'
+ int previousPosition = this.index;
+ readChar(); // first '*'
+ char nextCharacter= readChar(); // second '*'
+
+ // Init local variables
+ this.astLengthPtr = -1;
+ this.astPtr = -1;
+ this.currentTokenType = -1;
+ this.inlineTagStarted = false;
+ this.inlineTagStart = -1;
+ this.lineStarted = false;
+ this.returnStatement = null;
+ this.inherited = false;
+ this.deprecated = false;
+ this.linePtr = getLineNumber(javadocStart);
+ this.lastLinePtr = getLineNumber(javadocEnd);
+ this.lineEnd = (this.linePtr == this.lastLinePtr) ? this.endComment : this.scanner.getLineEnd(this.linePtr);
+ this.textStart = -1;
+ char previousChar = 0;
+ int invalidTagLineEnd = -1;
+ int invalidInlineTagLineEnd = -1;
+
+ // Loop on each comment character
+ while (this.index < this.endComment) {
+ previousPosition = this.index;
+ previousChar = nextCharacter;
+
+ // Calculate line end (cannot use this.scanner.linePtr as scanner does not parse line ends again)
+ if (this.index > (this.lineEnd+1)) {
+ updateLineEnd();
+ }
+
+ // Read next char only if token was consumed
+ if (this.currentTokenType < 0) {
+ nextCharacter = readChar(); // consider unicodes
+ } else {
+ previousPosition = this.scanner.getCurrentTokenStartPosition();
+ switch (this.currentTokenType) {
+ case TerminalTokens.TokenNameRBRACE:
+ nextCharacter = '}';
+ break;
+ case TerminalTokens.TokenNameMULTIPLY:
+ nextCharacter = '*';
+ break;
+ default:
+ nextCharacter = this.scanner.currentCharacter;
+ }
+ consumeToken();
+ }
+
+ if (this.index >= this.endComment) {
+ break;
+ }
+
+ switch (nextCharacter) {
+ case '@' :
+ boolean valid = false;
+ // Start tag parsing only if we have a java identifier start character and if we are on line beginning or at inline tag beginning
+ if ((!this.lineStarted || previousChar == '{')) {
+ this.lineStarted = true;
+ if (this.inlineTagStarted) {
+ this.inlineTagStarted = false;
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=53279
+ // Cannot have @ inside inline comment
+ if (this.sourceParser != null) {
+ int end = previousPosition<invalidInlineTagLineEnd ? previousPosition : invalidInlineTagLineEnd;
+ this.sourceParser.problemReporter().javadocUnterminatedInlineTag(this.inlineTagStart, end);
+ }
+ validComment = false;
+ if (this.lineStarted && this.textStart != -1 && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ if (this.kind == DOM_PARSER) refreshInlineTagPosition(previousPosition);
+ }
+ if (previousChar == '{') {
+ if (this.textStart != -1 && this.textStart < this.inlineTagStart) {
+ pushText(this.textStart, this.inlineTagStart);
+ }
+ this.inlineTagStarted = true;
+ invalidInlineTagLineEnd = this.lineEnd;
+ } else if (this.textStart != -1 && this.textStart < invalidTagLineEnd) {
+ pushText(this.textStart, invalidTagLineEnd);
+ }
+ this.scanner.resetTo(this.index, this.endComment);
+ this.currentTokenType = -1; // flush token cache at line begin
+ try {
+ int token = readTokenAndConsume();
+ this.tagSourceStart = this.scanner.getCurrentTokenStartPosition();
+ this.tagSourceEnd = this.scanner.getCurrentTokenEndPosition();
+ char[] tag = this.scanner.getCurrentIdentifierSource(); // first token is either an identifier or a keyword
+ if (this.kind == DOM_PARSER) {
+ // For DOM parser, try to get tag name other than java identifier
+ // (see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=51660)
+ int tk = token;
+ int le = this.lineEnd;
+ char pc = peekChar();
+ tagNameToken: while (tk != TerminalTokens.TokenNameEOF) {
+ this.tagSourceEnd = this.scanner.getCurrentTokenEndPosition();
+ token = tk;
+ // !, ", #, %, &, ', -, :, <, >, * chars and spaces are not allowed in tag names
+ switch (pc) {
+ case '}':
+ case '!':
+ case '#':
+ case '%':
+ case '&':
+ case '\'':
+ case ':':
+ // case '-': allowed in tag names as this character is often used in doclets (bug 68087)
+ case '<':
+ case '>':
+ case '*': // break for '*' as this is perhaps the end of comment (bug 65288)
+ break tagNameToken;
+ default:
+ if (pc == ' ' || Character.isWhitespace(pc)) break tagNameToken;
+ }
+ tk = readTokenAndConsume();
+ pc = peekChar();
+ }
+ int length = this.tagSourceEnd-this.tagSourceStart+1;
+ tag = new char[length];
+ System.arraycopy(this.source, this.tagSourceStart, tag, 0, length);
+ this.index = this.tagSourceEnd+1;
+ this.scanner.currentPosition = this.tagSourceEnd+1;
+ this.tagSourceStart = previousPosition;
+ this.lineEnd = le;
+ }
+ switch (token) {
+ case TerminalTokens.TokenNameIdentifier :
+ if (CharOperation.equals(tag, TAG_DEPRECATED)) {
+ this.deprecated = true;
+ if (this.kind == DOM_PARSER) {
+ valid = parseTag();
+ } else {
+ valid = true;
+ }
+ } else if (CharOperation.equals(tag, TAG_INHERITDOC)) {
+ // inhibits inherited flag when tags have been already stored
+ // see bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=51606
+ // Note that for DOM_PARSER, nodes stack may be not empty even no '@' tag
+ // was encountered in comment. But it cannot be the case for COMPILER_PARSER
+ // and so is enough as it is only this parser which signals the missing tag warnings...
+ this.inherited = this.astPtr==-1;
+ if (this.kind == DOM_PARSER) {
+ valid = parseTag();
+ } else {
+ valid = true;
+ }
+ } else if (CharOperation.equals(tag, TAG_PARAM)) {
+ valid = parseParam();
+ } else if (CharOperation.equals(tag, TAG_EXCEPTION)) {
+ valid = parseThrows(false);
+ } else if (CharOperation.equals(tag, TAG_SEE)) {
+ if (this.inlineTagStarted) {
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=53290
+ // Cannot have @see inside inline comment
+ valid = false;
+ if (this.sourceParser != null)
+ this.sourceParser.problemReporter().javadocUnexpectedTag(this.tagSourceStart, this.tagSourceEnd);
+ } else {
+ valid = parseSee(false);
+ }
+ } else if (CharOperation.equals(tag, TAG_LINK)) {
+ if (this.inlineTagStarted) {
+ valid = parseSee(false);
+ } else {
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=53290
+ // Cannot have @link outside inline comment
+ valid = false;
+ if (this.sourceParser != null)
+ this.sourceParser.problemReporter().javadocUnexpectedTag(this.tagSourceStart, this.tagSourceEnd);
+ }
+ } else if (CharOperation.equals(tag, TAG_LINKPLAIN)) {
+ if (this.inlineTagStarted) {
+ valid = parseSee(true);
+ } else {
+ valid = parseTag();
+ }
+ } else {
+ valid = parseTag();
+ }
+ break;
+ case TerminalTokens.TokenNamereturn :
+ valid = parseReturn();
+ // verify characters after return tag (we're expecting text description)
+ if(!verifyCharsAfterReturnTag(this.index)) {
+ if (this.sourceParser != null) {
+ int end = this.starPosition == -1 || this.lineEnd<this.starPosition ? this.lineEnd : this.starPosition;
+ this.sourceParser.problemReporter().javadocInvalidTag(this.tagSourceStart, end);
+ }
+ }
+ break;
+ case TerminalTokens.TokenNamethrows :
+ valid = parseThrows(true);
+ break;
+ default:
+ if (this.kind == DOM_PARSER) {
+ switch (token) {
+ case TerminalTokens.TokenNameabstract:
+ case TerminalTokens.TokenNameassert:
+ case TerminalTokens.TokenNameboolean:
+ case TerminalTokens.TokenNamebreak:
+ case TerminalTokens.TokenNamebyte:
+ case TerminalTokens.TokenNamecase:
+ case TerminalTokens.TokenNamecatch:
+ case TerminalTokens.TokenNamechar:
+ case TerminalTokens.TokenNameclass:
+ case TerminalTokens.TokenNamecontinue:
+ case TerminalTokens.TokenNamedefault:
+ case TerminalTokens.TokenNamedo:
+ case TerminalTokens.TokenNamedouble:
+ case TerminalTokens.TokenNameelse:
+ case TerminalTokens.TokenNameextends:
+ case TerminalTokens.TokenNamefalse:
+ case TerminalTokens.TokenNamefinal:
+ case TerminalTokens.TokenNamefinally:
+ case TerminalTokens.TokenNamefloat:
+ case TerminalTokens.TokenNamefor:
+ case TerminalTokens.TokenNameif:
+ case TerminalTokens.TokenNameimplements:
+ case TerminalTokens.TokenNameimport:
+ case TerminalTokens.TokenNameinstanceof:
+ case TerminalTokens.TokenNameint:
+ case TerminalTokens.TokenNameinterface:
+ case TerminalTokens.TokenNamelong:
+ case TerminalTokens.TokenNamenative:
+ case TerminalTokens.TokenNamenew:
+ case TerminalTokens.TokenNamenull:
+ case TerminalTokens.TokenNamepackage:
+ case TerminalTokens.TokenNameprivate:
+ case TerminalTokens.TokenNameprotected:
+ case TerminalTokens.TokenNamepublic:
+ case TerminalTokens.TokenNameshort:
+ case TerminalTokens.TokenNamestatic:
+ case TerminalTokens.TokenNamestrictfp:
+ case TerminalTokens.TokenNamesuper:
+ case TerminalTokens.TokenNameswitch:
+ case TerminalTokens.TokenNamesynchronized:
+ case TerminalTokens.TokenNamethis:
+ case TerminalTokens.TokenNamethrow:
+ case TerminalTokens.TokenNametransient:
+ case TerminalTokens.TokenNametrue:
+ case TerminalTokens.TokenNametry:
+ case TerminalTokens.TokenNamevoid:
+ case TerminalTokens.TokenNamevolatile:
+ case TerminalTokens.TokenNamewhile:
+ valid = parseTag();
+ break;
+ }
+ }
+ }
+ this.textStart = this.index;
+ if (!valid) {
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=51600
+ // do not stop the inline tag when error is encountered to get text after
+ validComment = false;
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=51600
+ // for DOM AST node, store tag as text in case of invalid syntax
+ if (this.kind == DOM_PARSER) {
+ parseTag();
+ this.textStart = this.tagSourceEnd+1;
+ invalidTagLineEnd = this.lineEnd;
+ }
+ }
+ } catch (InvalidInputException e) {
+ consumeToken();
+ }
+ }
+ break;
+ case '\r':
+ case '\n':
+ if (this.lineStarted && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ this.lineStarted = false;
+ // Fix bug 51650
+ this.textStart = -1;
+ break;
+ case '}' :
+ if (this.inlineTagStarted) {
+ if (this.lineStarted && this.textStart != -1 && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ if (this.kind == DOM_PARSER) refreshInlineTagPosition(previousPosition);
+ this.textStart = this.index;
+ this.inlineTagStarted = false;
+ } else {
+ if (!this.lineStarted) {
+ this.textStart = previousPosition;
+ }
+ }
+ this.lineStarted = true;
+ break;
+ case '{' :
+ if (this.inlineTagStarted) {
+ this.inlineTagStarted = false;
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=53279
+ // Cannot have opening brace in inline comment
+ if (this.sourceParser != null) {
+ int end = previousPosition<invalidInlineTagLineEnd ? previousPosition : invalidInlineTagLineEnd;
+ this.sourceParser.problemReporter().javadocUnterminatedInlineTag(this.inlineTagStart, end);
+ }
+ if (this.lineStarted && this.textStart != -1 && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ if (this.kind == DOM_PARSER) refreshInlineTagPosition(previousPosition);
+ }
+ if (!this.lineStarted) {
+ this.textStart = previousPosition;
+ }
+ this.lineStarted = true;
+ this.inlineTagStart = previousPosition;
+ break;
+ case '*' :
+ case '\u000c' : /* FORM FEED */
+ case ' ' : /* SPACE */
+ case '\t' : /* HORIZONTAL TABULATION */
+ // do nothing for space or '*' characters
+ break;
+ default :
+ if (!this.lineStarted) {
+ this.textStart = previousPosition;
+ }
+ this.lineStarted = true;
+ break;
+ }
+ }
+ // bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=53279
+ // Cannot leave comment inside inline comment
+ if (this.inlineTagStarted) {
+ this.inlineTagStarted = false;
+ if (this.sourceParser != null) {
+ int end = previousPosition<invalidInlineTagLineEnd ? previousPosition : invalidInlineTagLineEnd;
+ if (this.index >= this.endComment) end = invalidInlineTagLineEnd;
+ this.sourceParser.problemReporter().javadocUnterminatedInlineTag(this.inlineTagStart, end);
+ }
+ if (this.lineStarted && this.textStart != -1 && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ if (this.kind == DOM_PARSER) {
+ refreshInlineTagPosition(previousPosition);
+ }
+ } else if (this.lineStarted && this.textStart < previousPosition) {
+ pushText(this.textStart, previousPosition);
+ }
+ updateDocComment();
+ } catch (Exception ex) {
+ validComment = false;
+ }
+ return validComment;
+ }
+
+ private void consumeToken() {
+ this.currentTokenType = -1; // flush token cache
+ updateLineEnd();
+ }
+
+ protected abstract Object createArgumentReference(char[] name, int dim, Object typeRef, long[] dimPos, long argNamePos) throws InvalidInputException;
+ protected abstract Object createFieldReference(Object receiver) throws InvalidInputException;
+ protected abstract Object createMethodReference(Object receiver, List arguments) throws InvalidInputException;
+ protected Object createReturnStatement() { return null; }
+ protected abstract Object createTypeReference(int primitiveToken);
+
+ private int getEndPosition() {
+ if (this.scanner.getCurrentTokenEndPosition() > this.lineEnd) {
+ return this.lineEnd;
+ } else {
+ return this.scanner.getCurrentTokenEndPosition();
+ }
+ }
+
+ /*
+ * Search the source position corresponding to the end of a given line number.
+ * Warning: returned position is 1-based index!
+ * @see Scanner#getLineEnd(int) We cannot directly use this method
+ * when linePtr field is not initialized.
+ *
+ private int getLineEnd(int lineNumber) {
+
+ if (this.scanner.linePtr != -1) {
+ return this.scanner.getLineEnd(lineNumber);
+ }
+ if (this.lineEnds == null)
+ return -1;
+ if (lineNumber > this.lineEnds.length+1)
+ return -1;
+ if (lineNumber <= 0)
+ return -1;
+ if (lineNumber == this.lineEnds.length + 1)
+ return this.scanner.eofPosition;
+ return this.lineEnds[lineNumber-1]; // next line start one character behind the lineEnd of the previous line
+ }
+ */
+
+ /**
+ * Search the line number corresponding to a specific position.
+ * Warning: returned position is 1-based index!
+ * @see Scanner#getLineNumber(int) We cannot directly use this method
+ * when linePtr field is not initialized.
+ */
+ private int getLineNumber(int position) {
+
+ if (this.scanner.linePtr != -1) {
+ return this.scanner.getLineNumber(position);
+ }
+ if (this.lineEnds == null)
+ return 1;
+ int length = this.lineEnds.length;
+ if (length == 0)
+ return 1;
+ int g = 0, d = length - 1;
+ int m = 0;
+ while (g <= d) {
+ m = (g + d) /2;
+ if (position < this.lineEnds[m]) {
+ d = m-1;
+ } else if (position > this.lineEnds[m]) {
+ g = m+1;
+ } else {
+ return m + 1;
+ }
+ }
+ if (position < this.lineEnds[m]) {
+ return m+1;
+ }
+ return m+2;
+ }
+
+ /*
+ * Parse argument in @see tag method reference
+ */
+ private Object parseArguments(Object receiver) throws InvalidInputException {
+
+ // Init
+ int modulo = 0; // should be 2 for (Type,Type,...) or 3 for (Type arg,Type arg,...)
+ int iToken = 0;
+ char[] argName = null;
+ List arguments = new ArrayList(10);
+ int start = this.scanner.getCurrentTokenStartPosition();
+
+ // Parse arguments declaration if method reference
+ nextArg : while (this.index < this.scanner.eofPosition) {
+
+ // Read argument type reference
+ Object typeRef;
+ try {
+ typeRef = parseQualifiedName(false);
+ } catch (InvalidInputException e) {
+ break nextArg;
+ }
+ boolean firstArg = modulo == 0;
+ if (firstArg) { // verify position
+ if (iToken != 0)
+ break nextArg;
+ } else if ((iToken % modulo) != 0) {
+ break nextArg;
+ }
+ if (typeRef == null) {
+ if (firstArg && this.currentTokenType == TerminalTokens.TokenNameRPAREN) {
+ // verify characters after arguments declaration (expecting white space or end comment)
+ if (!verifySpaceOrEndComment()) {
+ int end = this.starPosition == -1 ? this.lineEnd : this.starPosition;
+ if (this.source[end]=='\n') end--;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMalformedSeeReference(start, end);
+ return null;
+ }
+ this.lineStarted = true;
+ return createMethodReference(receiver, null);
+ }
+ break nextArg;
+ }
+ iToken++;
+
+ // Read possible array declaration
+ int dim = 0;
+ long[] dimPositions = new long[20]; // assume that there won't be more than 20 dimensions...
+ if (readToken() == TerminalTokens.TokenNameLBRACKET) {
+ int dimStart = this.scanner.getCurrentTokenStartPosition();
+ while (readToken() == TerminalTokens.TokenNameLBRACKET) {
+ consumeToken();
+ if (readToken() != TerminalTokens.TokenNameRBRACKET) {
+ break nextArg;
+ }
+ consumeToken();
+ dimPositions[dim++] = (((long) dimStart) << 32) + this.scanner.getCurrentTokenEndPosition();
+ }
+ }
+
+ // Read argument name
+ long argNamePos = -1;
+ if (readToken() == TerminalTokens.TokenNameIdentifier) {
+ consumeToken();
+ if (firstArg) { // verify position
+ if (iToken != 1)
+ break nextArg;
+ } else if ((iToken % modulo) != 1) {
+ break nextArg;
+ }
+ if (argName == null) { // verify that all arguments name are declared
+ if (!firstArg) {
+ break nextArg;
+ }
+ }
+ argName = this.scanner.getCurrentIdentifierSource();
+ argNamePos = (((long)this.scanner.getCurrentTokenStartPosition())<<32)+this.scanner.getCurrentTokenEndPosition();
+ iToken++;
+ } else if (argName != null) { // verify that no argument name is declared
+ break nextArg;
+ }
+
+ // Verify token position
+ if (firstArg) {
+ modulo = iToken + 1;
+ } else {
+ if ((iToken % modulo) != (modulo - 1)) {
+ break nextArg;
+ }
+ }
+
+ // Read separator or end arguments declaration
+ int token = readToken();
+ char[] name = argName == null ? new char[0] : argName;
+ if (token == TerminalTokens.TokenNameCOMMA) {
+ // Create new argument
+ Object argument = createArgumentReference(name, dim, typeRef, dimPositions, argNamePos);
+ arguments.add(argument);
+ consumeToken();
+ iToken++;
+ } else if (token == TerminalTokens.TokenNameRPAREN) {
+ // verify characters after arguments declaration (expecting white space or end comment)
+ if (!verifySpaceOrEndComment()) {
+ int end = this.starPosition == -1 ? this.lineEnd : this.starPosition;
+ if (this.source[end]=='\n') end--;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMalformedSeeReference(start, end);
+ return null;
+ }
+ // Create new argument
+ Object argument = createArgumentReference(name, dim, typeRef, dimPositions, argNamePos);
+ arguments.add(argument);
+ consumeToken();
+ return createMethodReference(receiver, arguments);
+ } else {
+ break nextArg;
+ }
+ }
+
+ // Something wrong happened => Invalid input
+ throw new InvalidInputException();
+ }
+
+ /*
+ * Parse an URL link reference in @see tag
+ */
+ private boolean parseHref() throws InvalidInputException {
+ int start = this.scanner.getCurrentTokenStartPosition();
+ if (Character.toLowerCase(readChar()) == 'a') {
+ this.scanner.currentPosition = this.index;
+ if (readToken() == TerminalTokens.TokenNameIdentifier) {
+ this.currentTokenType = -1; // do not update line end
+ try {
+ if (CharOperation.equals(this.scanner.getCurrentIdentifierSource(), new char[]{'h', 'r', 'e', 'f'}, false) &&
+ readToken() == TerminalTokens.TokenNameEQUAL) {
+ this.currentTokenType = -1; // do not update line end
+ if (readToken() == TerminalTokens.TokenNameStringLiteral) {
+ this.currentTokenType = -1; // do not update line end
+ // Skip all characters after string literal until closing '>' (see bug 68726)
+ while (this.index <= this.lineEnd && readToken() != TerminalTokens.TokenNameGREATER) {
+ this.currentTokenType = -1; // do not update line end
+ }
+ if (this.currentTokenType == TerminalTokens.TokenNameGREATER) {
+ consumeToken(); // update line end as new lines are allowed in URL description
+ while (readToken() != TerminalTokens.TokenNameLESS) {
+ if (this.scanner.currentPosition >= this.scanner.eofPosition || this.scanner.currentCharacter == '@') {
+ // Reset position: we want to rescan last token
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ // Signal syntax error
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeUrlReference(start, this.lineEnd);
+ return false;
+ }
+ consumeToken();
+ }
+ this.currentTokenType = -1; // do not update line end
+ if (readChar() == '/') {
+ if (Character.toLowerCase(readChar()) == 'a') {
+ if (readChar() == '>') {
+ // Valid href
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ } catch (InvalidInputException ex) {
+ // Do nothing as we want to keep positions for error message
+ }
+ }
+ }
+ // Reset position: we want to rescan last token
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ // Signal syntax error
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeUrlReference(start, this.lineEnd);
+ return false;
+ }
+
+ /*
+ * Parse a method reference in @see tag
+ */
+ private Object parseMember(Object receiver) throws InvalidInputException {
+ // Init
+ this.identifierPtr = -1;
+ this.identifierLengthPtr = -1;
+ int start = this.scanner.getCurrentTokenStartPosition();
+ this.memberStart = start;
+
+ // Get member identifier
+ if (readToken() == TerminalTokens.TokenNameIdentifier) {
+ consumeToken();
+ pushIdentifier(true);
+ // Look for next token to know whether it's a field or method reference
+ int previousPosition = this.index;
+ if (readToken() == TerminalTokens.TokenNameLPAREN) {
+ consumeToken();
+ start = this.scanner.getCurrentTokenStartPosition();
+ try {
+ return parseArguments(receiver);
+ } catch (InvalidInputException e) {
+ int end = this.scanner.getCurrentTokenEndPosition() < this.lineEnd ?
+ this.scanner.getCurrentTokenEndPosition() :
+ this.scanner.getCurrentTokenStartPosition();
+ end = end < this.lineEnd ? end : this.lineEnd;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReferenceArgs(start, end);
+ }
+ return null;
+ }
+
+ // Reset position: we want to rescan last token
+ this.index = previousPosition;
+ this.scanner.currentPosition = previousPosition;
+ this.currentTokenType = -1;
+
+ // Verify character(s) after identifier (expecting space or end comment)
+ if (!verifySpaceOrEndComment()) {
+ int end = this.starPosition == -1 ? this.lineEnd : this.starPosition;
+ if (this.source[end]=='\n') end--;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMalformedSeeReference(start, end);
+ return null;
+ }
+ return createFieldReference(receiver);
+ }
+ int end = getEndPosition() - 1;
+ end = start > end ? start : end;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReference(start, end);
+ // Reset position: we want to rescan last token
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ return null;
+ }
+
+ /*
+ * Parse @param tag declaration
+ */
+ protected boolean parseParam() {
+
+ // Store current token state
+ int start = this.tagSourceStart;
+ int end = this.tagSourceEnd;
+
+ try {
+ // Push identifier next
+ int token = readToken();
+ switch (token) {
+ case TerminalTokens.TokenNameIdentifier :
+ consumeToken();
+ return pushParamName();
+ case TerminalTokens.TokenNameEOF :
+ break;
+ default :
+ start = this.scanner.getCurrentTokenStartPosition();
+ end = getEndPosition();
+ if (end < start) start = this.tagSourceStart;
+ break;
+ }
+ } catch (InvalidInputException e) {
+ end = getEndPosition();
+ }
+
+ // Reset position to avoid missing tokens when new line was encountered
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+
+ // Report problem
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMissingParamName(start, end);
+ return false;
+ }
+
+ /*
+ * Parse a qualified name and built a type reference if the syntax is valid.
+ */
+ protected Object parseQualifiedName(boolean reset) throws InvalidInputException {
+
+ // Reset identifier stack if requested
+ if (reset) {
+ this.identifierPtr = -1;
+ this.identifierLengthPtr = -1;
+ }
+
+ // Scan tokens
+ int primitiveToken = -1;
+ nextToken : for (int iToken = 0; ; iToken++) {
+ int token = readToken();
+ switch (token) {
+ case TerminalTokens.TokenNameIdentifier :
+ if (((iToken % 2) > 0)) { // identifiers must be odd tokens
+ break nextToken;
+ }
+ pushIdentifier(iToken == 0);
+ consumeToken();
+ break;
+
+ case TerminalTokens.TokenNameDOT :
+ if ((iToken % 2) == 0) { // dots must be even tokens
+ throw new InvalidInputException();
+ }
+ consumeToken();
+ break;
+
+ case TerminalTokens.TokenNamevoid :
+ case TerminalTokens.TokenNameboolean :
+ case TerminalTokens.TokenNamebyte :
+ case TerminalTokens.TokenNamechar :
+ case TerminalTokens.TokenNamedouble :
+ case TerminalTokens.TokenNamefloat :
+ case TerminalTokens.TokenNameint :
+ case TerminalTokens.TokenNamelong :
+ case TerminalTokens.TokenNameshort :
+ if (iToken > 0) {
+ throw new InvalidInputException();
+ }
+ pushIdentifier(true);
+ primitiveToken = token;
+ consumeToken();
+ break nextToken;
+
+ default :
+ if (iToken == 0) {
+ return null;
+ }
+ if ((iToken % 2) == 0) { // cannot leave on a dot
+ // Reset position: we want to rescan last token
+ if (this.kind == DOM_PARSER && this.currentTokenType != -1) {
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ }
+ throw new InvalidInputException();
+ }
+ break nextToken;
+ }
+ }
+ // Reset position: we want to rescan last token
+ if (this.currentTokenType != -1) {
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ }
+ this.lastIdentifierEndPosition = (int) this.identifierPositionStack[this.identifierPtr];
+ return createTypeReference(primitiveToken);
+ }
+
+ /*
+ * Parse a reference in @see tag
+ */
+ protected boolean parseReference(boolean plain) throws InvalidInputException {
+ Object typeRef = null;
+ Object reference = null;
+ int previousPosition = -1;
+ int typeRefStartPosition = -1;
+ nextToken : while (this.index < this.scanner.eofPosition) {
+ previousPosition = this.index;
+ int token = readToken();
+ switch (token) {
+ case TerminalTokens.TokenNameStringLiteral : // @see "string"
+ int start = this.scanner.getCurrentTokenStartPosition();
+ consumeToken();
+ // If typeRef != null we may raise a warning here to let user know there's an unused reference...
+ // Currently as javadoc 1.4.2 ignore it, we do the same (see bug 69302)
+ if (typeRef != null) {
+ start = this.tagSourceEnd+1;
+ previousPosition = start;
+ typeRef = null;
+ }
+ // verify end line (expecting empty or end comment)
+ if (verifyEndLine(previousPosition)) {
+ return true;
+ }
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReference(start, this.lineEnd);
+ return false;
+ case TerminalTokens.TokenNameLESS : // @see "<a href="URL#Value">label</a>
+ consumeToken();
+ start = this.scanner.getCurrentTokenStartPosition();
+ if (parseHref()) {
+ consumeToken();
+ // If typeRef != null we may raise a warning here to let user know there's an unused reference...
+ // Currently as javadoc 1.4.2 ignore it, we do the same (see bug 69302)
+ if (typeRef != null) {
+ start = this.tagSourceEnd+1;
+ previousPosition = start;
+ typeRef = null;
+ }
+ // verify end line (expecting empty or end comment)
+ if (verifyEndLine(previousPosition)) {
+ return true;
+ }
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReference(start, this.lineEnd);
+ }
+ return false;
+ case TerminalTokens.TokenNameERROR :
+ if (this.scanner.currentCharacter == '#') { // @see ...#member
+ consumeToken();
+ reference = parseMember(typeRef);
+ if (reference != null) {
+ return pushSeeRef(reference, plain);
+ }
+ return false;
+ }
+ break nextToken;
+ case TerminalTokens.TokenNameIdentifier :
+ if (typeRef == null) {
+ typeRefStartPosition = this.scanner.getCurrentTokenStartPosition();
+ typeRef = parseQualifiedName(true);
+ break;
+ }
+ break nextToken;
+ default :
+ break nextToken;
+ }
+ }
+
+ // Verify that we got a reference
+ if (reference == null) reference = typeRef;
+ if (reference == null) {
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMissingSeeReference(this.tagSourceStart, this.tagSourceEnd);
+ return false;
+ }
+
+ // Reset position at the end of type reference
+ this.index = this.lastIdentifierEndPosition+1;
+ this.scanner.currentPosition = this.index;
+ this.currentTokenType = -1;
+
+ // Verify that line end does not start with an open parenthese (which could be a constructor reference wrongly written...)
+ // See bug https://bugs.eclipse.org/bugs/show_bug.cgi?id=47215
+ char ch = peekChar();
+ if (ch == '(') {
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReference(typeRefStartPosition, this.lineEnd);
+ return false;
+ }
+
+ // Verify that we get white space after reference
+ if (!verifySpaceOrEndComment()) {
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ int end = this.starPosition == -1 ? this.lineEnd : this.starPosition;
+ if (this.source[end]=='\n') end--;
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMalformedSeeReference(typeRefStartPosition, end);
+ return false;
+ }
+
+ // Everything is OK, store reference
+ return pushSeeRef(reference, plain);
+ }
+
+ /*
+ * Parse @return tag declaration
+ */
+ protected abstract boolean parseReturn();
+
+ /*
+ * Parse @see tag declaration
+ */
+ protected boolean parseSee(boolean plain) {
+ int start = this.scanner.currentPosition;
+ try {
+ return parseReference(plain);
+ } catch (InvalidInputException ex) {
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidSeeReference(start, getEndPosition());
+ }
+ // Reset position to avoid missing tokens when new line was encountered
+ this.index = this.tokenPreviousPosition;
+ this.scanner.currentPosition = this.tokenPreviousPosition;
+ this.currentTokenType = -1;
+ return false;
+ }
+
+ /*
+ * Parse @return tag declaration
+ */
+ protected abstract boolean parseTag();
+
+ /*
+ * Parse @throws tag declaration
+ */
+ protected boolean parseThrows(boolean real) {
+ int start = this.scanner.currentPosition;
+ try {
+ Object typeRef = parseQualifiedName(true);
+ if (typeRef == null) {
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocMissingThrowsClassName(this.tagSourceStart, this.tagSourceEnd);
+ } else {
+ return pushThrowName(typeRef, real);
+ }
+ } catch (InvalidInputException ex) {
+ if (this.sourceParser != null) this.sourceParser.problemReporter().javadocInvalidThrowsClass(start, getEndPosition());
+ }
+ return false;
+ }
+
+ /*
+ * Return current character without move index position.
+ */
+ private char peekChar() {
+ int idx = this.index;
+ char c = this.source[idx++];
+ if (c == '\\' && this.source[idx] == 'u') {
+ int c1, c2, c3, c4;
+ idx++;
+ while (this.source[idx] == 'u')
+ idx++;
+ if (!(((c1 = Character.getNumericValue(this.source[idx++])) > 15 || c1 < 0)
+ || ((c2 = Character.getNumericValue(this.source[idx++])) > 15 || c2 < 0)
+ || ((c3 = Character.getNumericValue(this.source[idx++])) > 15 || c3 < 0) || ((c4 = Character.getNumericValue(this.source[idx++])) > 15 || c4 < 0))) {
+ c = (char) (((c1 * 16 + c2) * 16 + c3) * 16 + c4);
+ }
+ }
+ return c;
+ }
+
+ /*
+ * push the consumeToken on the identifier stack. Increase the total number of identifier in the stack.
+ */
+ protected void pushIdentifier(boolean newLength) {
+
+ int stackLength = this.identifierStack.length;
+ if (++this.identifierPtr >= stackLength) {
+ System.arraycopy(
+ this.identifierStack, 0,
+ this.identifierStack = new char[stackLength + 10][], 0,
+ stackLength);
+ System.arraycopy(
+ this.identifierPositionStack, 0,
+ this.identifierPositionStack = new long[stackLength + 10], 0,
+ stackLength);
+ }
+ this.identifierStack[this.identifierPtr] = this.scanner.getCurrentIdentifierSource();
+ this.identifierPositionStack[this.identifierPtr] = (((long) this.scanner.startPosition) << 32) + (this.scanner.currentPosition - 1);
+
+ if (newLength) {
+ stackLength = this.identifierLengthStack.length;
+ if (++this.identifierLengthPtr >= stackLength) {
+ System.arraycopy(
+ this.identifierLengthStack, 0,
+ this.identifierLengthStack = new int[stackLength + 10], 0,
+ stackLength);
+ }
+ this.identifierLengthStack[this.identifierLengthPtr] = 1;
+ } else {
+ this.identifierLengthStack[this.identifierLengthPtr]++;
+ }
+ }
+
+ /*
+ * Add a new obj on top of the ast stack.
+ * If new length is required, then add also a new length in length stack.
+ */
+ protected void pushOnAstStack(Object node, boolean newLength) {
+
+ if (node == null) {
+ this.astLengthStack[++this.astLengthPtr] = 0;
+ return;
+ }
+
+ int stackLength = this.astStack.length;
+ if (++this.astPtr >= stackLength) {
+ System.arraycopy(
+ this.astStack, 0,
+ this.astStack = new Object[stackLength + AstStackIncrement], 0,
+ stackLength);
+ this.astPtr = stackLength;
+ }
+ this.astStack[this.astPtr] = node;
+
+ if (newLength) {
+ stackLength = this.astLengthStack.length;
+ if (++this.astLengthPtr >= stackLength) {
+ System.arraycopy(
+ this.astLengthStack, 0,
+ this.astLengthStack = new int[stackLength + AstStackIncrement], 0,
+ stackLength);
+ }
+ this.astLengthStack[this.astLengthPtr] = 1;
+ } else {
+ this.astLengthStack[this.astLengthPtr]++;
+ }
+ }
+
+ /*
+ * Push a param name in ast node stack.
+ */
+ protected abstract boolean pushParamName();
+
+ /*
+ * Push a reference statement in ast node stack.
+ */
+ protected abstract boolean pushSeeRef(Object statement, boolean plain);
+
+ /*
+ * Push a text element in ast node stack
+ */
+ protected abstract void pushText(int start, int end);
+
+ /*
+ * Push a throws type ref in ast node stack.
+ */
+ protected abstract boolean pushThrowName(Object typeRef, boolean real);
+
+ /*
+ * Read current character and move index position.
+ * Warning: scanner position is unchanged using this method!
+ */
+ protected char readChar() {
+
+ char c = this.source[this.index++];
+ if (c == '\\' && this.source[this.index] == 'u') {
+ int c1, c2, c3, c4;
+ int pos = this.index;
+ this.index++;
+ while (this.source[this.index] == 'u')
+ this.index++;
+ if (!(((c1 = Character.getNumericValue(this.source[this.index++])) > 15 || c1 < 0)
+ || ((c2 = Character.getNumericValue(this.source[this.index++])) > 15 || c2 < 0)
+ || ((c3 = Character.getNumericValue(this.source[this.index++])) > 15 || c3 < 0) || ((c4 = Character.getNumericValue(this.source[this.index++])) > 15 || c4 < 0))) {
+ c = (char) (((c1 * 16 + c2) * 16 + c3) * 16 + c4);
+ } else {
+ // TODO (frederic) currently reset to previous position, perhaps signal a syntax error would be more appropriate
+ this.index = pos;
+ }
+ }
+ return c;
+ }
+
+ /*
+ * Read token only if previous was consumed
+ */
+ private int readToken() throws InvalidInputException {
+ if (this.currentTokenType < 0) {
+ this.tokenPreviousPosition = this.scanner.currentPosition;
+ this.currentTokenType = this.scanner.getNextToken();
+ if (this.scanner.currentPosition > (this.lineEnd+1)) { // be sure to be on next line (lineEnd is still on the same line)
+ this.lineStarted = false;
+ while (this.currentTokenType == TerminalTokens.TokenNameMULTIPLY) {
+ this.currentTokenType = this.scanner.getNextToken();
+ }
+ }
+ this.index = this.scanner.currentPosition;
+ this.lineStarted = true; // after having read a token, line is obviously started...
+ }
+ return this.currentTokenType;
+ }
+
+ private int readTokenAndConsume() throws InvalidInputException {
+ int token = readToken();
+ consumeToken();
+ return token;
+ }
+
+ /*
+ * Refresh start position and length of an inline tag.
+ */
+ protected void refreshInlineTagPosition(int previousPosition) {
+ // do nothing by default
+ }
+
+ public String toString() {
+ StringBuffer buffer = new StringBuffer();
+ int startPos = this.scanner.currentPosition<this.index ? this.scanner.currentPosition : this.index;
+ int endPos = this.scanner.currentPosition<this.index ? this.index : this.scanner.currentPosition;
+ if (startPos == this.source.length)
+ return "EOF\n\n" + new String(this.source); //$NON-NLS-1$
+ if (endPos > this.source.length)
+ return "behind the EOF\n\n" + new String(this.source); //$NON-NLS-1$
+
+ char front[] = new char[startPos];
+ System.arraycopy(this.source, 0, front, 0, startPos);
+
+ int middleLength = (endPos - 1) - startPos + 1;
+ char middle[];
+ if (middleLength > -1) {
+ middle = new char[middleLength];
+ System.arraycopy(
+ this.source,
+ startPos,
+ middle,
+ 0,
+ middleLength);
+ } else {
+ middle = CharOperation.NO_CHAR;
+ }
+
+ char end[] = new char[this.source.length - (endPos - 1)];
+ System.arraycopy(
+ this.source,
+ (endPos - 1) + 1,
+ end,
+ 0,
+ this.source.length - (endPos - 1) - 1);
+
+ buffer.append(front);
+ if (this.scanner.currentPosition<this.index) {
+ buffer.append("\n===============================\nScanner current position here -->"); //$NON-NLS-1$
+ } else {
+ buffer.append("\n===============================\nParser index here -->"); //$NON-NLS-1$
+ }
+ buffer.append(middle);
+ if (this.scanner.currentPosition<this.index) {
+ buffer.append("<-- Parser index here\n===============================\n"); //$NON-NLS-1$
+ } else {
+ buffer.append("<-- Scanner current position here\n===============================\n"); //$NON-NLS-1$
+ }
+ buffer.append(end);
+
+ return buffer.toString();
+ }
+
+ /*
+ * Update
+ */
+ protected abstract void updateDocComment();
+
+ /*
+ * Update line end
+ */
+ protected void updateLineEnd() {
+ while (this.index > (this.lineEnd+1)) { // be sure to be on next line (lineEnd is still on the same line)
+ if (this.linePtr < this.lastLinePtr) {
+ this.lineEnd = this.scanner.getLineEnd(++this.linePtr) - 1;
+ } else {
+ this.lineEnd = this.endComment;
+ return;
+ }
+ }
+ }
+
+ /*
+ * Verify that end of the line only contains space characters or end of comment.
+ * Note that end of comment may be preceeding by several contiguous '*' chars.
+ */
+ private boolean verifyEndLine(int textPosition) {
+ int startPosition = this.index;
+ int previousPosition = this.index;
+ this.starPosition = -1;
+ char ch = readChar();
+ nextChar: while (true) {
+ switch (ch) {
+ case '\r':
+ case '\n':
+ if (this.kind == DOM_PARSER) {
+ parseTag();
+ pushText(textPosition, previousPosition);
+ }
+ this.index = previousPosition;
+ return true;
+ case '\u000c' : /* FORM FEED */
+ case ' ' : /* SPACE */
+ case '\t' : /* HORIZONTAL TABULATION */
+ if (this.starPosition >= 0) break nextChar;
+ break;
+ case '*':
+ this.starPosition = previousPosition;
+ break;
+ case '/':
+ if (this.starPosition >= textPosition) {
+ if (this.kind == DOM_PARSER) {
+ parseTag();
+ pushText(textPosition, this.starPosition);
+ }
+ return true;
+ }
+ default :
+ // leave loop
+ break nextChar;
+
+ }
+ previousPosition = this.index;
+ ch = readChar();
+ }
+ this.index = startPosition;
+ return false;
+ }
+
+ /*
+ * Verify that some text exists after a @return tag. Text must be different than
+ * end of comment which may be preceeding by several '*' chars.
+ */
+ private boolean verifyCharsAfterReturnTag(int startPosition) {
+ // Whitespace or inline tag closing brace
+ int previousPosition = this.index;
+ char ch = readChar();
+ boolean malformed = true;
+ while (Character.isWhitespace(ch)) {
+ malformed = false;
+ previousPosition = this.index;
+ ch = readChar();
+ }
+ // End of comment
+ this.starPosition = -1;
+ nextChar: while (this.index<this.source.length) {
+ switch (ch) {
+ case '*':
+ // valid whatever the number of star before last '/'
+ this.starPosition = previousPosition;
+ break;
+ case '/':
+ if (this.starPosition >= startPosition) { // valid only if a star was previous character
+ return false;
+ }
+ default :
+ // valid if any other character is encountered, even white spaces
+ this.index = startPosition;
+ return !malformed;
+
+ }
+ previousPosition = this.index;
+ ch = readChar();
+ }
+ this.index = startPosition;
+ return false;
+ }
+
+ /*
+ * Verify characters after a name matches one of following conditions:
+ * 1- first character is a white space
+ * 2- first character is a closing brace *and* we're currently parsing an inline tag
+ * 3- are the end of comment (several contiguous star ('*') characters may be
+ * found before the last slash ('/') character).
+ */
+ private boolean verifySpaceOrEndComment() {
+ int startPosition = this.index;
+ // Whitespace or inline tag closing brace
+ char ch = peekChar();
+ switch (ch) {
+ case '}':
+ return this.inlineTagStarted;
+ default:
+ if (Character.isWhitespace(ch)) {
+ return true;
+ }
+ }
+ // End of comment
+ int previousPosition = this.index;
+ this.starPosition = -1;
+ ch = readChar();
+ nextChar: while (this.index<this.source.length) {
+ switch (ch) {
+ case '*':
+ // valid whatever the number of star before last '/'
+ this.starPosition = previousPosition;
+ break;
+ case '/':
+ if (this.starPosition >= startPosition) { // valid only if a star was previous character
+ return true;
+ }
+ default :
+ // invalid whatever other character, even white spaces
+ this.index = startPosition;
+ return false;
+
+ }
+ previousPosition = this.index;
+ ch = readChar();
+ }
+ this.index = startPosition;
+ return false;
+ }
+}