/* * Copyright 2015 The Closure Compiler Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.javascript.jscomp; import static com.google.javascript.rhino.TypeDeclarationsIR.anyType; import static com.google.javascript.rhino.TypeDeclarationsIR.arrayType; import static com.google.javascript.rhino.TypeDeclarationsIR.booleanType; import static com.google.javascript.rhino.TypeDeclarationsIR.functionType; import static com.google.javascript.rhino.TypeDeclarationsIR.namedType; import static com.google.javascript.rhino.TypeDeclarationsIR.numberType; import static com.google.javascript.rhino.TypeDeclarationsIR.optionalParameter; import static com.google.javascript.rhino.TypeDeclarationsIR.parameterizedType; import static com.google.javascript.rhino.TypeDeclarationsIR.recordType; import static com.google.javascript.rhino.TypeDeclarationsIR.stringType; import static com.google.javascript.rhino.TypeDeclarationsIR.undefinedType; import static com.google.javascript.rhino.TypeDeclarationsIR.unionType; import com.google.common.base.Function; import com.google.common.base.Predicates; import com.google.common.collect.FluentIterable; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.javascript.jscomp.NodeTraversal.AbstractPostOrderCallback; import com.google.javascript.rhino.IR; import com.google.javascript.rhino.JSDocInfo; import com.google.javascript.rhino.JSTypeExpression; import com.google.javascript.rhino.Node; import com.google.javascript.rhino.Node.TypeDeclarationNode; import com.google.javascript.rhino.Token; import java.util.LinkedHashMap; import javax.annotation.Nullable; /** * Converts JS with types in jsdocs to an extended JS syntax that includes types. * (Still keeps the jsdocs intact.) * * @author alexeagle@google.com (Alex Eagle) * * TODO(alexeagle): handle inline-style JSDoc annotations as well. */ public final class JsdocToEs6TypedConverter extends AbstractPostOrderCallback implements CompilerPass { private final AbstractCompiler compiler; public JsdocToEs6TypedConverter(AbstractCompiler compiler) { this.compiler = compiler; } @Override public void process(Node externs, Node root) { NodeTraversal.traverseEs6(compiler, root, this); } @Override public void visit(NodeTraversal t, Node n, Node parent) { JSDocInfo bestJSDocInfo = NodeUtil.getBestJSDocInfo(n); switch (n.getToken()) { case FUNCTION: if (bestJSDocInfo != null) { setTypeExpression(n, bestJSDocInfo.getReturnType()); } break; case NAME: case GETPROP: if (parent == null) { break; } if (parent.isVar() || parent.isAssign() || parent.isExprResult()) { if (bestJSDocInfo != null) { setTypeExpression(n, bestJSDocInfo.getType()); } } else if (parent.isParamList()) { JSDocInfo parentDocInfo = NodeUtil.getBestJSDocInfo(parent.getParent()); if (parentDocInfo == null) { break; } JSTypeExpression parameterType = parentDocInfo.getParameterType(n.getString()); if (parameterType != null) { Node attachTypeExpr = n; // Modify the primary AST to represent a function parameter as a // REST node, if the type indicates it is a rest parameter. if (parameterType.getRoot().getToken() == Token.ELLIPSIS) { attachTypeExpr = IR.rest(n.getString()); n.replaceWith(attachTypeExpr); compiler.reportChangeToEnclosingScope(attachTypeExpr); } setTypeExpression(attachTypeExpr, parameterType); } } break; default: break; } } private void setTypeExpression(Node n, JSTypeExpression type) { TypeDeclarationNode node = TypeDeclarationsIRFactory.convert(type); if (node != null) { n.setDeclaredTypeExpression(node); if (n.isFunction()) { compiler.reportChangeToEnclosingScope(n.getFirstChild()); } else { compiler.reportChangeToEnclosingScope(n); } } } /** * Converts root nodes of JSTypeExpressions into TypeDeclaration ASTs. * * @author alexeagle@google.com (Alex Eagle) */ public static final class TypeDeclarationsIRFactory { // Allow functional-style Iterables.transform over collections of nodes. private static final Function<Node, TypeDeclarationNode> CONVERT_TYPE_NODE = new Function<Node, TypeDeclarationNode>() { @Override public TypeDeclarationNode apply(Node node) { return convertTypeNodeAST(node); } }; @Nullable public static TypeDeclarationNode convert(@Nullable JSTypeExpression typeExpr) { if (typeExpr == null) { return null; } return convertTypeNodeAST(typeExpr.getRoot()); } /** * The root of a JSTypeExpression is very different from an AST node, even * though we use the same Java class to represent them. * This function converts root nodes of JSTypeExpressions into TypeDeclaration ASTs, * to make them more similar to ordinary AST nodes. * * @return the root node of a TypeDeclaration AST, or null if no type is * available for the node. */ // TODO(dimvar): Eventually, we want to just parse types to the new // representation directly, and delete this function. @Nullable public static TypeDeclarationNode convertTypeNodeAST(Node n) { Token token = n.getToken(); switch (token) { case STAR: case EMPTY: // for function types that don't declare a return type return anyType(); case VOID: return undefinedType(); case BANG: // TODO(alexeagle): non-nullable is assumed to be the default return convertTypeNodeAST(n.getFirstChild()); case STRING: String typeName = n.getString(); switch (typeName) { case "boolean": return booleanType(); case "number": return numberType(); case "string": return stringType(); case "null": case "undefined": case "void": return null; default: TypeDeclarationNode root = namedType(typeName); if (n.hasChildren() && n.getFirstChild().isNormalBlock()) { Node block = n.getFirstChild(); if ("Array".equals(typeName)) { return arrayType(convertTypeNodeAST(block.getFirstChild())); } return parameterizedType(root, Iterables.filter( Iterables.transform(block.children(), CONVERT_TYPE_NODE), Predicates.notNull())); } return root; } case QMARK: Node child = n.getFirstChild(); return child == null ? anyType() // For now, our ES6_TYPED language doesn't support nullable // so we drop it before building the tree. // : nullable(convertTypeNodeAST(child)); : convertTypeNodeAST(child); case LC: LinkedHashMap<String, TypeDeclarationNode> properties = new LinkedHashMap<>(); for (Node field : n.getFirstChild().children()) { boolean isFieldTypeDeclared = field.getToken() == Token.COLON; Node fieldNameNode = isFieldTypeDeclared ? field.getFirstChild() : field; String fieldName = fieldNameNode.getString(); if (fieldName.startsWith("'") || fieldName.startsWith("\"")) { fieldName = fieldName.substring(1, fieldName.length() - 1); } TypeDeclarationNode fieldType = isFieldTypeDeclared ? convertTypeNodeAST(field.getLastChild()) : null; properties.put(fieldName, fieldType); } return recordType(properties); case ELLIPSIS: return arrayType(convertTypeNodeAST(n.getFirstChild())); case PIPE: ImmutableList<TypeDeclarationNode> types = FluentIterable .from(n.children()).transform(CONVERT_TYPE_NODE) .filter(Predicates.notNull()).toList(); switch (types.size()) { case 0: return null; case 1: return types.get(0); default: return unionType(types); } case FUNCTION: Node returnType = anyType(); LinkedHashMap<String, TypeDeclarationNode> requiredParams = new LinkedHashMap<>(); LinkedHashMap<String, TypeDeclarationNode> optionalParams = new LinkedHashMap<>(); String restName = null; TypeDeclarationNode restType = null; for (Node child2 : n.children()) { if (child2.isParamList()) { int paramIdx = 1; for (Node param : child2.children()) { String paramName = "p" + paramIdx++; if (param.getToken() == Token.ELLIPSIS) { if (param.getFirstChild() != null) { restType = arrayType(convertTypeNodeAST(param.getFirstChild())); } restName = paramName; } else { TypeDeclarationNode paramNode = convertTypeNodeAST(param); if (paramNode.getToken() == Token.OPTIONAL_PARAMETER) { optionalParams.put(paramName, (TypeDeclarationNode) paramNode.removeFirstChild()); } else { requiredParams.put(paramName, convertTypeNodeAST(param)); } } } } else if (child2.isNew()) { // TODO(alexeagle): keep the constructor signatures on the tree, and emit them following // the syntax in TypeScript 1.4 spec, section 3.7.8 Constructor Type Literals } else if (child2.isThis()) { // Not expressible in TypeScript syntax, so we omit them from the tree. // They could be added as properties on the result node. } else { returnType = convertTypeNodeAST(child2); } } return functionType(returnType, requiredParams, optionalParams, restName, restType); case EQUALS: TypeDeclarationNode optionalParam = convertTypeNodeAST(n.getFirstChild()); return optionalParam == null ? null : optionalParameter(optionalParam); default: throw new IllegalArgumentException( "Unsupported node type: " + n.getToken() + " " + n.toStringTree()); } } } }