| /******************************************************************************* |
| * Copyright (c) 2011 NumberFour AG |
| * |
| * 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: |
| * NumberFour AG - initial API and Implementation (Alex Panchenko) |
| *******************************************************************************/ |
| package org.eclipse.dltk.javascript.typeinfo; |
| |
| import static org.eclipse.dltk.javascript.parser.util.CharStreamUtil.match; |
| import static org.eclipse.dltk.javascript.parser.util.CharStreamUtil.skipSpaces; |
| |
| import java.text.ParseException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.antlr.runtime.ANTLRStringStream; |
| import org.antlr.runtime.CharStream; |
| import org.eclipse.dltk.internal.javascript.ti.JSDocProblem; |
| import org.eclipse.dltk.internal.javascript.validation.ValidationMessages; |
| import org.eclipse.dltk.javascript.core.JavaScriptProblems; |
| import org.eclipse.dltk.javascript.typeinfo.model.FunctionType; |
| import org.eclipse.dltk.javascript.typeinfo.model.JSType; |
| import org.eclipse.dltk.javascript.typeinfo.model.Parameter; |
| import org.eclipse.dltk.javascript.typeinfo.model.ParameterKind; |
| import org.eclipse.dltk.javascript.typeinfo.model.RecordProperty; |
| import org.eclipse.dltk.javascript.typeinfo.model.RecordType; |
| import org.eclipse.dltk.javascript.typeinfo.model.SimpleType; |
| import org.eclipse.dltk.javascript.typeinfo.model.TypeInfoModelFactory; |
| import org.eclipse.dltk.javascript.typeinfo.model.TypeInfoModelLoader; |
| import org.eclipse.dltk.javascript.typeinfo.model.TypeInfoModelPackage; |
| import org.eclipse.dltk.javascript.typeinfo.model.UnionType; |
| import org.eclipse.emf.common.util.EList; |
| import org.eclipse.osgi.util.NLS; |
| |
| public class JSDocTypeParser { |
| |
| public static final String FUNCTION = "function"; |
| public static final String CLASS = "Class"; |
| |
| private final char[] extensionChars; |
| |
| public JSDocTypeParser() { |
| this(null); |
| } |
| |
| /** |
| * Creates this type parser specifying the characters which potentially |
| * could lead the syntax extensions. If such a character occurs in the type |
| * name, then {@link #parseExtension(CharStream, int)} is called and can |
| * continue the parsing. |
| * |
| * @param extensionChars |
| */ |
| protected JSDocTypeParser(char[] extensionChars) { |
| this.extensionChars = extensionChars; |
| } |
| |
| private JSDocTypeParserExtension extension; |
| |
| public void setExtension(JSDocTypeParserExtension extension) { |
| this.extension = extension; |
| } |
| |
| public JSType parse(String input) throws ParseException { |
| final ANTLRStringStream stream = new ANTLRStringStream(input); |
| final JSType type = parse(stream, true); |
| if (stream.LT(1) != CharStream.EOF) { |
| throw new ParseException("Unexpected " |
| + stream.substring(stream.index(), stream.size() - 1), |
| stream.index()); |
| } |
| return type; |
| } |
| |
| /** |
| * Parses the next type expression. {@link UnionType}s are handled at this |
| * level, parsing of parts is delegated to {@link #parseType(CharStream)}. |
| * |
| * @param input |
| * @param autoUnion |
| * if <code>true</code> then it is allowed to parse all parts of |
| * the union type, but if <code>false</code> then unit type |
| * declaration should be enclosed in parenthesis - |
| * <code>'('</code> and <code>')'</code>. |
| */ |
| public JSType parse(CharStream input, boolean autoUnion) |
| throws ParseException { |
| skipSpaces(input); |
| final List<JSType> types = new ArrayList<JSType>(); |
| final boolean inParenthese = input.LT(1) == '('; |
| if (inParenthese) { |
| input.consume(); |
| skipSpaces(input); |
| } |
| for (;;) { |
| final JSType type = parseType(input); |
| if (type != null) { |
| types.add(type); |
| skipSpaces(input); |
| if ((inParenthese || autoUnion) && input.LT(1) == '|') { |
| input.consume(); |
| skipSpaces(input); |
| continue; |
| } |
| } |
| break; |
| } |
| if (inParenthese) { |
| match(input, ')'); |
| } |
| if (types.size() == 1) { |
| return types.get(0); |
| } else if (types.size() > 1) { |
| final UnionType unionType = TypeInfoModelFactory.eINSTANCE |
| .createUnionType(); |
| unionType.getTargets().addAll(types); |
| return unionType; |
| } else { |
| return null; |
| } |
| } |
| |
| protected JSType parseType(CharStream input) throws ParseException { |
| if (input.LT(1) == '?') { |
| // Nullable type |
| input.consume(); |
| } else if (input.LT(1) == '!') { |
| // Non-nullable type |
| input.consume(); |
| } |
| if (input.LT(1) == '{') { |
| input.consume(); |
| final RecordType recordType = parseRecordType(input); |
| match(input, '}'); |
| return recordType; |
| } else if (input.LT(1) == '*') { |
| input.consume(); |
| return TypeInfoModelFactory.eINSTANCE.createAnyType(); |
| } else { |
| return parseTypeName(input); |
| } |
| } |
| |
| protected JSType parseTypeName(CharStream input) throws ParseException { |
| final int start = input.index(); |
| int ch; |
| int end; |
| for (;;) { |
| ch = input.LT(1); |
| if (extensionChars != null) { |
| for (int i = 0; i < extensionChars.length; ++i) { |
| if (extensionChars[i] == ch) { |
| final JSType result = parseExtension(input, start); |
| if (result != null) { |
| if (extension != null) { |
| extension.reportType(result, start, |
| input.index()); |
| } |
| return result; |
| } |
| break; |
| } |
| } |
| } |
| if (Character.isWhitespace(ch)) { |
| end = input.index(); |
| skipSpaces(input); |
| ch = input.LT(1); |
| } else if (ch == '<' || ch == '(' || ch == '[' |
| || ch == CharStream.EOF || ch == '|' || ch == ',' |
| || ch == '=' || ch == '}' || ch == '>' || ch == ')' |
| || ch == ']') { |
| end = input.index(); |
| } else if (ch == '.' && input.LT(2) == '<') { |
| end = input.index(); |
| } else { |
| input.consume(); |
| continue; |
| } |
| break; |
| } |
| if (ch == '<') { |
| final String baseType = input.substring(start, end - 1); |
| input.consume(); |
| final List<JSType> typeParams = parseTypeParams(input); |
| match(input, '>'); |
| JSType type = createGenericType(baseType, typeParams); |
| if (extension != null) { |
| extension.reportType(type, start, end); |
| } |
| return checkIfArray(input, type); |
| } else if (ch == '.' && input.LT(2) == '<') { |
| final String baseType = input.substring(start, end - 1); |
| input.consume(); |
| input.consume(); |
| final List<JSType> typeParams = parseTypeParams(input); |
| match(input, '>'); |
| JSType type = createGenericType(baseType, typeParams); |
| if (extension != null) { |
| extension.reportType(type, start, end); |
| } |
| return checkIfArray(input, type); |
| } else if (ch == '(' |
| && FUNCTION.equals(input.substring(start, end - 1))) { |
| input.consume(); |
| final FunctionType functionType = TypeInfoModelFactory.eINSTANCE |
| .createFunctionType(); |
| parseFunctionParams(input, functionType.getParameters()); |
| match(input, ')'); |
| skipSpaces(input); |
| if (input.LT(1) == ':') { |
| input.consume(); |
| skipSpaces(input); |
| functionType.setReturnType(parse(input, false)); |
| } |
| return checkIfArray(input, functionType); |
| } else if (ch == '[') { |
| final JSType itemType = createType(input, start, end); |
| input.consume(); |
| match(input, ']'); |
| final JSType array = createArray(itemType); |
| return checkIfArray(input, array); |
| } else { |
| return end > start ? createType(input, start, end) : null; |
| } |
| } |
| |
| /** |
| * Parses the syntax extension, returns the parsed type or <code>null</code> |
| * if parsing should continue in the normal way. This method is called if |
| * current character (<code>input.LT(1)</code>) is equal to one of the |
| * characters passed to the constructor. |
| * |
| * @param input |
| * the character stream, <code>input.LT(1)</code> |
| * @param start |
| * @return |
| * @throws ParseException |
| */ |
| protected JSType parseExtension(CharStream input, int start) |
| throws ParseException { |
| return null; |
| } |
| |
| private JSType createType(CharStream input, final int start, final int end) { |
| final JSType type = createType(translate(input |
| .substring(start, end - 1))); |
| if (extension != null) { |
| extension.reportType(type, start, end); |
| } |
| return type; |
| } |
| |
| /** |
| * @param input |
| * @param type |
| * @return |
| * @throws ParseException |
| */ |
| private JSType checkIfArray(CharStream input, JSType type) |
| throws ParseException { |
| int ch = input.LT(1); |
| while (ch == '[') { |
| input.consume(); |
| match(input, ']'); |
| type = createArray(type); |
| ch = input.LT(1); |
| } |
| return type; |
| } |
| |
| protected JSType createType(String typeName) { |
| if (ITypeNames.UNDEFINED.equals(typeName)) { |
| return TypeInfoModelFactory.eINSTANCE.createUndefinedType(); |
| } else if (CLASS.equals(typeName)) { |
| return TypeInfoModelFactory.eINSTANCE.createClassType(); |
| } else { |
| return TypeUtil.ref(typeName); |
| } |
| } |
| |
| protected JSType createArray(JSType itemType) { |
| return TypeUtil.arrayOf(itemType); |
| } |
| |
| protected JSType createGenericType(String baseType, List<JSType> typeParams) |
| throws ParseException { |
| if (ITypeNames.ARRAY.equals(baseType)) { |
| if (typeParams.size() != 1) { |
| throw new JSDocParseException( |
| NLS.bind( |
| ValidationMessages.IncorrectNumberOfTypeArguments, |
| ITypeNames.ARRAY), |
| JavaScriptProblems.PARAMETERIZED_TYPE_INCORRECT_ARGUMENTS); |
| } |
| return createArray(typeParams.get(0)); |
| } else if (CLASS.equals(baseType)) { |
| if (typeParams.size() != 1) { |
| throw new JSDocParseException( |
| NLS.bind( |
| ValidationMessages.IncorrectNumberOfTypeArguments, |
| CLASS), |
| JavaScriptProblems.PARAMETERIZED_TYPE_INCORRECT_ARGUMENTS); |
| } |
| final JSType typeParam = typeParams.get(0); |
| if (typeParam.eClass() != TypeInfoModelPackage.Literals.SIMPLE_TYPE) { |
| throw new JSDocParseException( |
| JSDocProblem.WRONG_TYPE_PARAMETERIZATION, CLASS, |
| typeParam.eClass().getName()); |
| } |
| return TypeUtil.classType(((SimpleType) typeParam).getTarget()); |
| } else if (ITypeNames.OBJECT.equals(baseType)) { |
| if (typeParams.isEmpty() || typeParams.size() > 2) { |
| throw new JSDocParseException( |
| NLS.bind( |
| ValidationMessages.IncorrectNumberOfTypeArguments, |
| ITypeNames.OBJECT), |
| JavaScriptProblems.PARAMETERIZED_TYPE_INCORRECT_ARGUMENTS); |
| } else if (typeParams.size() == 2) { |
| return TypeUtil.mapOf(typeParams.get(0), typeParams.get(1)); |
| } else { |
| assert typeParams.size() == 1; |
| return TypeUtil.mapOf(null, typeParams.get(0)); |
| } |
| } else { |
| return doCreateGenericType(baseType, typeParams); |
| } |
| } |
| |
| protected JSType doCreateGenericType(String baseType, |
| List<JSType> typeParams) throws ParseException { |
| if (!typeParams.isEmpty()) { |
| StringBuilder sb = new StringBuilder(); |
| sb.append(baseType); |
| sb.append('<'); |
| int index = 0; |
| for (JSType typeParam : typeParams) { |
| if (++index > 1) { |
| sb.append(','); |
| } |
| sb.append(typeParam.getName()); |
| } |
| sb.append('>'); |
| return TypeUtil.ref(sb.toString()); |
| } |
| return TypeUtil.ref(baseType); |
| } |
| |
| protected List<JSType> parseTypeParams(CharStream input) |
| throws ParseException { |
| final List<JSType> types = new ArrayList<JSType>(); |
| for (;;) { |
| final JSType type = parse(input, true); |
| if (type != null) { |
| types.add(type); |
| skipSpaces(input); |
| if (input.LT(1) == ',') { |
| input.consume(); |
| continue; |
| } |
| } |
| break; |
| } |
| return types; |
| } |
| |
| public void parseFunctionParams(CharStream input, |
| EList<Parameter> parameters) throws ParseException { |
| for (;;) { |
| // TODO support parameter names (at least "this") |
| skipSpaces(input); |
| boolean varargs = false; |
| boolean squareBracket = false; |
| if (input.LT(1) == '.' && input.LT(2) == '.' && input.LT(3) == '.') { |
| input.consume(); |
| input.consume(); |
| input.consume(); |
| varargs = true; |
| if (input.LT(1) == '[') { |
| input.consume(); |
| squareBracket = true; |
| } |
| } |
| final JSType type = parse(input, true); |
| if (type != null) { |
| if (input.LT(1) == '.' && input.LT(2) == '.' |
| && input.LT(3) == '.') { |
| input.consume(); |
| input.consume(); |
| input.consume(); |
| varargs = true; |
| if (input.LT(1) == '[') { |
| input.consume(); |
| squareBracket = true; |
| } |
| } |
| final Parameter parameter = TypeInfoModelFactory.eINSTANCE |
| .createParameter(); |
| parameter.setType(type); |
| if (varargs) { |
| parameter.setKind(ParameterKind.VARARGS); |
| if (squareBracket) { |
| match(input, ']'); |
| } |
| } else if (input.LT(1) == '=') { |
| parameter.setKind(ParameterKind.OPTIONAL); |
| input.consume(); |
| } |
| parameters.add(parameter); |
| skipSpaces(input); |
| if (input.LT(1) == ',') { |
| input.consume(); |
| continue; |
| } |
| } |
| break; |
| } |
| } |
| |
| protected RecordType parseRecordType(CharStream input) |
| throws ParseException { |
| final int start = input.index(); |
| final RecordType type = TypeInfoModelFactory.eINSTANCE |
| .createRecordType(); |
| skipSpaces(input); |
| for (;;) { |
| int ch = input.LT(1); |
| final boolean optional = ch == '['; |
| if (optional) { |
| input.consume(); |
| ch = input.LT(1); |
| } |
| boolean validPropertyName = true; |
| final int nameStart = input.index(); |
| String name = null; |
| if (ch == '"' || ch == '\'') { |
| input.consume(); |
| ch = input.LT(1); |
| while (ch != '"' && ch != '\'' && ch != -1) { |
| input.consume(); |
| ch = input.LT(1); |
| } |
| if (ch == -1) { |
| throw new ParseException("Ending quote expected", |
| input.index()); |
| } |
| validPropertyName = false; |
| name = input.substring(nameStart + 1, input.index() - 1); |
| input.consume(); |
| } else if (Character.isJavaIdentifierStart(ch)) { |
| input.consume(); |
| while (Character.isJavaIdentifierPart(input.LT(1))) { |
| input.consume(); |
| } |
| name = input.substring(nameStart, input.index() - 1); |
| } |
| if (name != null) { |
| final RecordProperty property = TypeInfoModelFactory.eINSTANCE |
| .createRecordProperty(); |
| property.setName(name); |
| skipSpaces(input); |
| if (optional) { |
| match(input, ']'); |
| skipSpaces(input); |
| property.setOptional(true); |
| } |
| |
| if (input.LT(1) == ':') { |
| input.consume(); |
| final JSType memberType = parse(input, true); |
| ch = input.LT(1); |
| if (ch == '=') { |
| input.consume(); |
| property.setOptional(true); |
| } |
| if (memberType != null) { |
| property.setType(memberType); |
| } else { |
| property.setType(TypeInfoModelFactory.eINSTANCE |
| .createAnyType()); |
| } |
| } else { |
| property.setType(TypeInfoModelFactory.eINSTANCE |
| .createAnyType()); |
| } |
| if (validPropertyName) |
| type.getMembers().add(property); |
| skipSpaces(input); |
| if (input.LT(1) == ',') { |
| input.consume(); |
| skipSpaces(input); |
| continue; |
| } |
| } |
| break; |
| } |
| type.setTypeName('{' + input.substring(start, input.index() - 1) + '}'); |
| return type; |
| } |
| |
| protected String translate(String typeName) { |
| return TypeInfoModelLoader.getInstance().translateTypeName(typeName); |
| } |
| |
| } |