Copyright (c) 2005, 2017 IBM Corporation and others. This program and the accompanying materials are made available under the terms of the Eclipse Public License 2.0 which accompanies this distribution, and is available at https://www.eclipse.org/legal/epl-2.0/ SPDX-License-Identifier: EPL-2.0 Contributors: IBM Corporation - initial API and implementation Stephan Herrmann - Contribution for Bug 425183 - [1.8][inference] make CaptureBinding18 safe Bug 466308 - [hovering] Javadoc header for parameter is wrong with annotation-based null analysis
/******************************************************************************* * Copyright (c) 2005, 2017 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Stephan Herrmann - Contribution for * Bug 425183 - [1.8][inference] make CaptureBinding18 safe * Bug 466308 - [hovering] Javadoc header for parameter is wrong with annotation-based null analysis *******************************************************************************/
package org.eclipse.jdt.internal.core.util; import org.eclipse.jdt.core.compiler.CharOperation; import org.eclipse.jdt.internal.compiler.ast.Wildcard; public class BindingKeyParser { int keyStart; static final char C_THROWN = '|'; static class Scanner { static final int PACKAGE = 0; static final int TYPE = 1; static final int FIELD = 2; static final int METHOD = 3; static final int ARRAY = 4; static final int LOCAL_VAR = 5; static final int FLAGS = 6; static final int WILDCARD = 7; static final int CAPTURE = 8; static final int CAPTURE18 = 9; static final int BASE_TYPE = 10; static final int MODULE = 11; static final int END = 12; static final int START = -1; int index = 0, start; char[] source; int token = START; Scanner(char[] source) { this.source = source; } char[] getTokenSource() { int length = this.index-this.start; char[] result = new char[length]; System.arraycopy(this.source, this.start, result, 0, length); return result; } boolean isAtAnnotationStart() { return this.index < this.source.length && this.source[this.index] == '@'; } boolean isAtCaptureStart() { return this.index < this.source.length && this.source[this.index] == '!'; } boolean isAtCapture18Start() { return this.index < this.source.length && this.source[this.index] == '^'; } boolean isAtFieldOrMethodStart() { return this.index < this.source.length && this.source[this.index] == '.'; } boolean isAtLocalVariableStart() { return this.index < this.source.length && this.source[this.index] == '#'; } boolean isAtMemberTypeStart() { return this.index < this.source.length && (this.source[this.index] == '$' || (this.source[this.index] == '.' && this.source[this.index-1] == '>')); } boolean isAtParametersEnd() { return this.index < this.source.length && this.source[this.index] == '>'; } boolean isAtParametersStart() { char currentChar; return this.index > 0 && this.index < this.source.length && ((currentChar = this.source[this.index]) == '<' || currentChar == '%'); } boolean isAtRawTypeEnd() { return this.index > 0 && this.index < this.source.length && this.source[this.index] == '>'; } boolean isAtSecondaryTypeStart() { return this.index < this.source.length && this.source[this.index] == '~'; } boolean isAtWildcardStart() { return this.index < this.source.length && this.source[this.index] == '{'; // e.g {1}+Ljava/lang/String; } boolean isAtTypeParameterStart() { return this.index < this.source.length && this.source[this.index] == 'T'; } boolean isAtTypeArgumentStart() { return this.index < this.source.length && "LIZVCDBFJS[!".indexOf(this.source[this.index]) != -1; //$NON-NLS-1$ } boolean isAtThrownStart() { return this.index < this.source.length && this.source[this.index] == C_THROWN; } boolean isAtTypeVariableStart() { return this.index < this.source.length && this.source[this.index] == ':'; } boolean isAtTypeWithCaptureStart() { return this.index < this.source.length && this.source[this.index] == '&'; } boolean isAtModuleStart() { return this.index < this.source.length && this.source[this.index] == '"'; } int nextToken() { int previousTokenEnd = this.index; this.start = this.index; int dollarIndex = -1; int length = this.source.length; while (this.index <= length) { char currentChar = this.index == length ? Character.MIN_VALUE : this.source[this.index]; switch (currentChar) { case 'B': case 'C': case 'D': case 'F': case 'I': case 'J': case 'N': case 'S': case 'V': case 'Z': // base type if (this.index == previousTokenEnd && (this.index == 0 || this.source[this.index-1] != '.')) { // case of field or method starting with one of the character above this.index++; this.token = BASE_TYPE; return this.token; } break; case 'L': case 'T': if (this.index == previousTokenEnd && (this.index == 0 || this.source[this.index-1] != '.')) { // case of field or method starting with one of the character above this.start = this.index+1; dollarIndex = -1; } break; case ';': if (this.index == previousTokenEnd) { this.start = this.index+1; dollarIndex = -1; previousTokenEnd = this.start; } else { if (dollarIndex != -1) this.index = dollarIndex; this.token = TYPE; return this.token; } break; case '$': if (this.index == previousTokenEnd) { this.start = this.index+1; dollarIndex = -1; } else { if (dollarIndex == -1) { dollarIndex = this.index; break; } this.index = dollarIndex; this.token = TYPE; return this.token; } break; case '~': if (this.index == previousTokenEnd) { this.start = this.index+1; dollarIndex = -1; } else { this.token = TYPE; return this.token; } break; case '.': if (this.token == MODULE) break; // don't treat '.' as a separator in module names //$FALL-THROUGH$ case '%': case ':': case '>': case '@': this.start = this.index+1; dollarIndex = -1; previousTokenEnd = this.start; break; case '[': while (this.index < length && this.source[this.index] == '[') this.index++; this.token = ARRAY; return this.token; case '<': if (this.start > 0) { switch (this.source[this.start-1]) { case '.': if (this.source[this.start-2] == '>') { // case of member type where enclosing type is parameterized if (dollarIndex != -1) this.index = dollarIndex; this.token = TYPE; } else { this.token = METHOD; } return this.token; default: if (this.index == previousTokenEnd) { this.start = this.index+1; dollarIndex = -1; previousTokenEnd = this.start; } else { if (dollarIndex != -1) this.index = dollarIndex; this.token = TYPE; return this.token; } } } break; case '(': this.token = METHOD; return this.token; case ')': if (this.token == TYPE) { this.token = FIELD; return this.token; } this.start = this.index+1; dollarIndex = -1; previousTokenEnd = this.start; break; case '#': if (this.index == previousTokenEnd) { this.start = this.index+1; dollarIndex = -1; previousTokenEnd = this.start; } else { this.token = LOCAL_VAR; return this.token; } break; case Character.MIN_VALUE: switch (this.token) { case START: this.token = PACKAGE; break; case METHOD: case LOCAL_VAR: this.token = LOCAL_VAR; break; case TYPE: if (this.index > this.start && this.source[this.start-1] == '.') this.token = FIELD; else this.token = END; break; case WILDCARD: this.token = TYPE; break; default: this.token = END; break; } return this.token; case '*': case '+': case '-': this.index++; this.token = WILDCARD; return this.token; case '!': case '&': this.index++; this.token = CAPTURE; return this.token; case '^': this.index++; this.token = CAPTURE18; return this.token; case '"': this.index++; this.token = MODULE; return this.token; } this.index++; } this.token = END; return this.token; } void skipMethodSignature() { this.start = this.index; int braket = 0; while (this.index < this.source.length) { switch (this.source[this.index]) { case '#': case '%': case '@': case C_THROWN: return; case ':': if (braket == 0) return; break; case '<': case '(': braket++; break; case '>': case ')': braket--; break; } this.index++; } } void skipRank() { this.start = this.index; while (this.index < this.source.length && "0123456789".indexOf(this.source[this.index]) != -1) //$NON-NLS-1$ this.index++; } void skipThrownStart() { while (this.index < this.source.length && this.source[this.index] == C_THROWN) this.index++; } void skipParametersStart() { while (this.index < this.source.length && (this.source[this.index] == '<' || this.source[this.index] == '%')) this.index++; } void skipParametersEnd() { while (this.index < this.source.length && this.source[this.index] != '>') this.index++; this.index++; } void skipTypeEnd() { if (this.index < this.source.length && this.source[this.index] == ';') this.index++; } void skipRankStart() { if (this.index < this.source.length && this.source[this.index] == '{') this.index++; } void skipRankEnd() { if (this.index < this.source.length && this.source[this.index] == '}') this.index++; this.start = this.index; } void skipCapture18Delim() { if (this.index < this.source.length && this.source[this.index] == '#') this.index++; this.start = this.index; } @Override public String toString() { StringBuffer buffer = new StringBuffer(); switch (this.token) { case START: buffer.append("START: "); //$NON-NLS-1$ break; case PACKAGE: buffer.append("PACKAGE: "); //$NON-NLS-1$ break; case TYPE: buffer.append("TYPE: "); //$NON-NLS-1$ break; case FIELD: buffer.append("FIELD: "); //$NON-NLS-1$ break; case METHOD: buffer.append("METHOD: "); //$NON-NLS-1$ break; case ARRAY: buffer.append("ARRAY: "); //$NON-NLS-1$ break; case LOCAL_VAR: buffer.append("LOCAL VAR: "); //$NON-NLS-1$ break; case FLAGS: buffer.append("MODIFIERS: "); //$NON-NLS-1$ break; case WILDCARD: buffer.append("WILDCARD: "); //$NON-NLS-1$ break; case CAPTURE: buffer.append("CAPTURE: "); //$NON-NLS-1$ break; case CAPTURE18: buffer.append("CAPTURE18: "); //$NON-NLS-1$ break; case BASE_TYPE: buffer.append("BASE TYPE: "); //$NON-NLS-1$ break; case MODULE: buffer.append("MODULE: "); //$NON-NLS-1$ break; case END: buffer.append("END: "); //$NON-NLS-1$ break; } if (this.index < 0) { buffer.append("**"); //$NON-NLS-1$ buffer.append(this.source); } else if (this.index <= this.source.length) { buffer.append(this.source, 0, this.start); buffer.append('*'); if (this.start <= this.index) { buffer.append(this.source, this.start, this.index - this.start); buffer.append('*'); buffer.append(this.source, this.index, this.source.length - this.index); } else { buffer.append('*'); buffer.append(this.source, this.start, this.source.length - this.start); } } else { buffer.append(this.source); buffer.append("**"); //$NON-NLS-1$ } return buffer.toString(); } } private boolean parsingPaused; private Scanner scanner; private boolean hasTypeName = true; private boolean hasModuleName; private boolean isMalformed; private boolean isParsingThrownExceptions = false; // https://bugs.eclipse.org/bugs/show_bug.cgi?id=336451 public BindingKeyParser(BindingKeyParser parser) { this(""); //$NON-NLS-1$ this.scanner = parser.scanner; } public BindingKeyParser(String key) { this.scanner = new Scanner(key.toCharArray()); } public void consumeAnnotation() { // default is to do nothing } public void consumeArrayDimension(char[] brakets) { // default is to do nothing } public void consumeBaseType(char[] baseTypeSig) { // default is to do nothing } public void consumeCapture(int position) { // default is to do nothing } public void consumeCapture18ID(int id, int position) { // default is to do nothing } public void consumeException() { // default is to do nothing } public void consumeField(char[] fieldName) { // default is to do nothing } public void consumeParameterizedGenericMethod() { // default is to do nothing } public void consumeLocalType(char[] uniqueKey) { // default is to do nothing } public void consumeLocalVar(char[] varName, int occurrenceCount, int argumentPosition) { // default is to do nothing } public void consumeMethod(char[] selector, char[] signature) { // default is to do nothing } public void consumeModifiers(char[] modifiers) { // default is to do nothing } public void consumeNonGenericType() { // default is to do nothing } public void consumeMemberType(char[] simpleTypeName) { // default is to do nothing } public void consumePackage(char[] pkgName) { // default is to do nothing } public void consumeParameterizedType(char[] simpleTypeName, boolean isRaw) { // default is to do nothing } public void consumeParser(BindingKeyParser parser) { // default is to do nothing } public void consumeRawType() { // default is to do nothing } public void consumeScope(int scopeNumber) { // default is to do nothing } public void consumeSecondaryType(char[] simpleTypeName) { // default is to do nothing } public void consumeFullyQualifiedName(char[] fullyQualifiedName) { // default is to do nothing } public void consumeKey() { // default is to do nothing } public void consumeTopLevelType() { // default is to do nothing } public void consumeType() { // default is to do nothing } public void consumeTypeParameter(char[] typeParameterName) { // default is to do nothing } public void consumeTypeVariable(char[] position, char[] typeVariableName) { // default is to do nothing } public void consumeTypeWithCapture() { // default is to do nothing } public void consumeWildCard(int kind) { // default is to do nothing } public void consumeWildcardRank(int rank) { // default is to do nothing } public void consumeModule(char[] moduleName) { // default is to do nothing } /* * Returns the string that this binding key wraps. */ public String getKey() { return new String(this.scanner.source); } public boolean hasTypeName() { return this.hasTypeName; } public boolean hasModuleName() { return this.hasModuleName; } public void malformedKey() { this.isMalformed = true; } public BindingKeyParser newParser() { return new BindingKeyParser(this); } public void parse() { parse(false/*don't pause after fully qualified name*/); } public void parse(boolean pauseAfterFullyQualifiedName) { if (!this.parsingPaused) { if (parseModule()) return; // fully qualified name parseFullyQualifiedName(); parseSecondaryType(); if (pauseAfterFullyQualifiedName) { this.parsingPaused = true; return; } } if (!hasTypeName()) { consumeKey(); return; } consumeTopLevelType(); parseInnerType(); if (this.scanner.isAtParametersStart()) { this.scanner.skipParametersStart(); if (this.scanner.isAtTypeParameterStart()) { // generic type parseGenericType(); // skip ";>" this.scanner.skipParametersEnd(); // local type in generic type parseInnerType(); } else if (this.scanner.isAtTypeArgumentStart()) // parameterized type parseParameterizedType(null/*top level type or member type with raw enclosing type*/, false/*no raw*/); else if (this.scanner.isAtRawTypeEnd()) // raw type parseRawType(); } else { // non-generic type consumeNonGenericType(); } consumeType(); this.scanner.skipTypeEnd(); if (this.scanner.isAtFieldOrMethodStart()) { switch (this.scanner.nextToken()) { case Scanner.FIELD: parseField(); if (this.scanner.isAtAnnotationStart()) { parseAnnotation(); } return; case Scanner.METHOD: parseMethod(); if (this.scanner.isAtLocalVariableStart()) { parseLocalVariable(); } else if (this.scanner.isAtTypeVariableStart()) { parseTypeVariable(); } else if (this.scanner.isAtAnnotationStart()) { parseAnnotation(); } break; default: malformedKey(); return; } } else if (!this.isParsingThrownExceptions && this.scanner.isAtTypeVariableStart()) { parseTypeVariable(); } else if (this.scanner.isAtWildcardStart()) { parseWildcard(); } else if (this.scanner.isAtTypeWithCaptureStart()) { parseTypeWithCapture(); } else if (this.scanner.isAtAnnotationStart()) { parseAnnotation(); } consumeKey(); } private boolean parseModule() { if (this.scanner.isAtModuleStart()) { this.hasTypeName = false; this.keyStart = 1; if (this.scanner.nextToken() == Scanner.MODULE && this.scanner.nextToken() == Scanner.END) { consumeModule(this.scanner.getTokenSource()); this.hasModuleName = true; return true; } malformedKey(); } return false; } private void parseFullyQualifiedName() { if (this.scanner.isAtCaptureStart()) { parseCapture(); this.hasTypeName = false; return; } if (this.scanner.isAtCapture18Start()) { parseCapture18(); this.hasTypeName = false; return; } switch(this.scanner.nextToken()) { case Scanner.PACKAGE: this.keyStart = 0; consumePackage(this.scanner.getTokenSource()); this.hasTypeName = false; return; case Scanner.TYPE: this.keyStart = this.scanner.start-1; consumeFullyQualifiedName(this.scanner.getTokenSource()); break; case Scanner.BASE_TYPE: this.keyStart = this.scanner.start-1; consumeBaseType(this.scanner.getTokenSource()); this.hasTypeName = false; break; case Scanner.ARRAY: this.keyStart = this.scanner.start; consumeArrayDimension(this.scanner.getTokenSource()); switch (this.scanner.nextToken()) { case Scanner.TYPE: consumeFullyQualifiedName(this.scanner.getTokenSource()); break; case Scanner.BASE_TYPE: consumeBaseType(this.scanner.getTokenSource()); this.hasTypeName = false; break; default: malformedKey(); return; } break; case Scanner.WILDCARD: // support the '-' in "Lpack/package-info;", see bug 398920 if (!CharOperation.endsWith(this.scanner.getTokenSource(), new char[] {'/', 'p', 'a', 'c', 'k', 'a', 'g', 'e', '-'})) { malformedKey(); return; } int start = this.scanner.start; if (this.scanner.nextToken() == Scanner.TYPE) { if (!CharOperation.equals(this.scanner.getTokenSource(), new char[] {'i', 'n', 'f', 'o'})) { malformedKey(); return; } this.scanner.start = start; this.keyStart = start-1; consumeFullyQualifiedName(this.scanner.getTokenSource()); break; } break; default: malformedKey(); return; } } private void parseParameterizedMethod() { this.scanner.skipParametersStart(); while (!this.scanner.isAtParametersEnd() && !this.isMalformed) { parseTypeArgument(); } consumeParameterizedGenericMethod(); } private void parseGenericType() { while (!this.scanner.isAtParametersEnd() && !this.isMalformed) { if (this.scanner.nextToken() != Scanner.TYPE) { malformedKey(); return; } consumeTypeParameter(this.scanner.getTokenSource()); this.scanner.skipTypeEnd(); } } private void parseInnerType() { if (!this.scanner.isAtMemberTypeStart() || this.scanner.nextToken() != Scanner.TYPE) return; char[] typeName = this.scanner.getTokenSource(); // Might not actually be an inner type but came here as a consequence of '$' being present in type name if (typeName.length == 0) return; if (Character.isDigit(typeName[0])) { // anonymous or local type int nextToken = Scanner.TYPE; while (this.scanner.isAtMemberTypeStart() && !this.isMalformed) nextToken = this.scanner.nextToken(); typeName = nextToken == Scanner.END ? this.scanner.source : CharOperation.subarray(this.scanner.source, this.keyStart, this.scanner.index+1); consumeLocalType(typeName); } else { consumeMemberType(typeName); parseInnerType(); } } private void parseLocalVariable() { if (this.scanner.nextToken() != Scanner.LOCAL_VAR) { malformedKey(); return; } char[] varName = this.scanner.getTokenSource(); if (Character.isDigit(varName[0])) { int index = Integer.parseInt(new String(varName)); consumeScope(index); if (!this.scanner.isAtLocalVariableStart()) { malformedKey(); return; } parseLocalVariable(); } else { int occurrenceCount = 0; if (this.scanner.isAtLocalVariableStart()) { if (this.scanner.nextToken() != Scanner.LOCAL_VAR) { malformedKey(); return; } char[] occurrence = this.scanner.getTokenSource(); occurrenceCount = Integer.parseInt(new String(occurrence)); } int position = -1; if (this.scanner.isAtLocalVariableStart()) { if (this.scanner.nextToken() != Scanner.LOCAL_VAR) { malformedKey(); return; } char[] posToken = this.scanner.getTokenSource(); position = Integer.parseInt(new String(posToken)); } consumeLocalVar(varName, occurrenceCount, position); } } private void parseMethod() { char[] selector = this.scanner.getTokenSource(); this.scanner.skipMethodSignature(); char[] signature = this.scanner.getTokenSource(); consumeMethod(selector, signature); if (this.scanner.isAtThrownStart()) { parseThrownExceptions(); } if (this.scanner.isAtParametersStart()) parseParameterizedMethod(); } private void parseAnnotation() { /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); consumeAnnotation(); this.isMalformed = parser.isMalformed; this.scanner.token = token; } private void parseCapture() { if (this.scanner.nextToken() != Scanner.CAPTURE) return; parseCaptureWildcard(); if (this.scanner.nextToken() != Scanner.TYPE) { malformedKey(); return; } char[] positionChars = this.scanner.getTokenSource(); int position = Integer.parseInt(new String(positionChars)); consumeCapture(position); this.scanner.skipTypeEnd(); } private void parseCapture18() { // syntax: ^{int#int} if (this.scanner.nextToken() != Scanner.CAPTURE18) return; this.scanner.skipRankStart(); // { this.scanner.skipRank(); char[] source = this.scanner.getTokenSource(); int position = Integer.parseInt(new String(source)); this.scanner.skipCapture18Delim(); // # this.scanner.skipRank(); source = this.scanner.getTokenSource(); int id = Integer.parseInt(new String(source)); this.scanner.skipRankEnd(); // } consumeCapture18ID(id, position); this.scanner.skipTypeEnd(); } private void parseCaptureWildcard() { /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); this.isMalformed = parser.isMalformed; this.scanner.token = token; } private void parseField() { char[] fieldName = this.scanner.getTokenSource(); parseReturnType(); consumeField(fieldName); } private void parseThrownExceptions() { /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; while (this.scanner.isAtThrownStart() && !this.isMalformed) { this.scanner.skipThrownStart(); BindingKeyParser parser = newParser(); parser.isParsingThrownExceptions = true; parser.parse(); consumeParser(parser); consumeException(); this.isMalformed = parser.isMalformed; } this.scanner.token = token; } private void parseParameterizedType(char[] typeName, boolean isRaw) { if (!isRaw) { while (!this.scanner.isAtParametersEnd() && !this.isMalformed) { parseTypeArgument(); } } // skip ";>" this.scanner.skipParametersEnd(); consumeParameterizedType(typeName, isRaw); this.scanner.skipTypeEnd(); if (this.scanner.isAtMemberTypeStart() && this.scanner.nextToken() == Scanner.TYPE) { typeName = this.scanner.getTokenSource(); if (this.scanner.isAtParametersStart()) { this.scanner.skipParametersStart(); parseParameterizedType(typeName, this.scanner.isAtRawTypeEnd()); } else consumeParameterizedType(typeName, true/*raw*/); } } private void parseRawType() { this.scanner.skipParametersEnd(); consumeRawType(); this.scanner.skipTypeEnd(); if (this.scanner.isAtMemberTypeStart() && this.scanner.nextToken() == Scanner.TYPE) { char[] typeName = this.scanner.getTokenSource(); if (this.scanner.isAtParametersStart()) { this.scanner.skipParametersStart(); parseParameterizedType(typeName, this.scanner.isAtRawTypeEnd()); } else consumeParameterizedType(typeName, true/*raw*/); } } private void parseReturnType() { this.scanner.index++; // skip ')' /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); this.isMalformed = parser.isMalformed; this.scanner.token = token; } private void parseSecondaryType() { if (!this.scanner.isAtSecondaryTypeStart() || this.scanner.nextToken() != Scanner.TYPE) return; consumeSecondaryType(this.scanner.getTokenSource()); } private void parseTypeArgument() { /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); this.isMalformed = parser.isMalformed; this.scanner.token = token; } private void parseTypeWithCapture() { if (this.scanner.nextToken() != Scanner.CAPTURE) return; /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); consumeTypeWithCapture(); this.isMalformed = parser.isMalformed; this.scanner.token = token; } private void parseTypeVariable() { if (this.scanner.nextToken() != Scanner.TYPE) { malformedKey(); return; } char[] typeVariableName = this.scanner.getTokenSource(); char[] position; int length = typeVariableName.length; if (length > 0 && Character.isDigit(typeVariableName[0])) { int firstT = CharOperation.indexOf('T', typeVariableName); position = CharOperation.subarray(typeVariableName, 0, firstT); typeVariableName = CharOperation.subarray(typeVariableName, firstT+1, typeVariableName.length); } else { position = CharOperation.NO_CHAR; } consumeTypeVariable(position, typeVariableName); this.scanner.skipTypeEnd(); } private void parseWildcard() { parseWildcardRank(); if (this.scanner.nextToken() != Scanner.WILDCARD) return; char[] source = this.scanner.getTokenSource(); if (source.length == 0) { malformedKey(); return; } int kind = -1; switch (source[0]) { case '*': kind = Wildcard.UNBOUND; break; case '+': kind = Wildcard.EXTENDS; break; case '-': kind = Wildcard.SUPER; break; } if (kind == -1) { malformedKey(); return; } if (kind != Wildcard.UNBOUND) parseWildcardBound(); consumeWildCard(kind); } private void parseWildcardRank() { this.scanner.skipRankStart(); this.scanner.skipRank(); char[] source = this.scanner.getTokenSource(); consumeWildcardRank(Integer.parseInt(new String(source))); this.scanner.skipRankEnd(); } private void parseWildcardBound() { /* * The call parser.parse() might have a side-effect on the current token type * See bug 264443 */ int token = this.scanner.token; BindingKeyParser parser = newParser(); parser.parse(); consumeParser(parser); this.isMalformed = parser.isMalformed; this.scanner.token = token; } }