/*
* 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 com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.javascript.jscomp.Es6RewriteClass.ClassDeclarationMetadata;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.JSDocInfoBuilder;
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 com.google.javascript.rhino.TypeDeclarationsIR;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* Converts {@link Node#getDeclaredTypeExpression()} to {@link JSDocInfo#getType()} type
* annotations. Types are marked as inline types.
*/
public final class Es6TypedToEs6Converter implements NodeTraversal.Callback, HotSwapCompilerPass {
static final DiagnosticType CANNOT_CONVERT_MEMBER_VARIABLES = DiagnosticType.error(
"JSC_CANNOT_CONVERT_FIELDS",
"Can only convert class member variables (fields) in declarations or the right hand side of "
+ "a simple assignment.");
static final DiagnosticType CANNOT_CONVERT_BOUNDED_GENERICS = DiagnosticType.warning(
"JSC_CANNOT_CONVERT_BOUNDED_GENERICS",
"Bounded generics are not yet implemented.");
static final DiagnosticType TYPE_ALIAS_ALREADY_DECLARED = DiagnosticType.error(
"JSC_TYPE_ALIAS_ALREADY_DECLARED",
"Type alias already declared as a variable: {0}");
static final DiagnosticType TYPE_QUERY_NOT_SUPPORTED = DiagnosticType.warning(
"JSC_TYPE_QUERY_NOT_SUPPORTED",
"Type query is currently not supported.");
static final DiagnosticType UNSUPPORTED_RECORD_TYPE = DiagnosticType.error(
"JSC_UNSUPPORTED_RECORD_TYPE",
"Currently only member variables are supported in record types, please consider "
+ "using interfaces instead.");
static final DiagnosticType COMPUTED_PROP_ACCESS_MODIFIER = DiagnosticType.warning(
"JSC_COMPUTED_PROP_ACCESS_MODIFIER",
"Accessibility is not checked on computed properties");
static final DiagnosticType NON_AMBIENT_NAMESPACE_NOT_SUPPORTED = DiagnosticType.error(
"JSC_NON_AMBIENT_NAMESPACE_NOT_SUPPORTED",
"Non-ambient namespaces are not supported");
static final DiagnosticType CALL_SIGNATURE_NOT_SUPPORTED = DiagnosticType.error(
"JSC_CALL_SIGNATURE_NOT_SUPPORTED",
"Call signature and construct signatures are not supported yet");
static final DiagnosticType OVERLOAD_NOT_SUPPORTED = DiagnosticType.warning(
"JSC_OVERLOAD_NOT_SUPPORTED",
"Function and method overloads are not supported and type information might be lost");
static final DiagnosticType SPECIALIZED_SIGNATURE_NOT_SUPPORTED = DiagnosticType.warning(
"JSC_SPECIALIZED_SIGNATURE_NOT_SUPPORTED",
"Specialized signatures are not supported and type information might be lost");
static final DiagnosticType DECLARE_IN_NON_EXTERNS = DiagnosticType.warning(
"JSC_DECLARE_IN_NON_EXTERNS",
"Found a declare statement in program code.\n"
+ "If you are generating externs, this should be fine.\n"
+ "If not, make sure to pass your .d.ts file as an extern file.");
private final AbstractCompiler compiler;
private final Map<Node, Namespace> nodeNamespaceMap;
private final Set<String> convertedNamespaces;
private Namespace currNamespace;
private final Deque<Map<String, Node>> overloadStack;
private final Set<Node> processedOverloads;
Es6TypedToEs6Converter(AbstractCompiler compiler) {
this.compiler = compiler;
this.nodeNamespaceMap = new HashMap<>();
this.convertedNamespaces = new HashSet<>();
this.overloadStack = new ArrayDeque<>();
this.processedOverloads = new HashSet<>();
}
@Override
public void process(Node externs, Node scriptRoot) {
ScanNamespaces scanner = new ScanNamespaces();
NodeTraversal.traverseEs6(compiler, externs, scanner);
NodeTraversal.traverseEs6(compiler, scriptRoot, scanner);
NodeTraversal.traverseEs6(compiler, externs, this);
NodeTraversal.traverseEs6(compiler, scriptRoot, this);
}
@Override
public void hotSwapScript(Node scriptRoot, Node originalRoot) {
ScanNamespaces scanner = new ScanNamespaces();
NodeTraversal.traverseEs6(compiler, scriptRoot, scanner);
NodeTraversal.traverseEs6(compiler, scriptRoot, this);
}
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
if (NodeUtil.isStatementParent(n)) {
pushOverloads();
}
switch (n.getToken()) {
case NAMESPACE:
if (currNamespace == null && parent.getToken() != Token.DECLARE) {
compiler.report(JSError.make(n, NON_AMBIENT_NAMESPACE_NOT_SUPPORTED));
return false;
}
currNamespace = nodeNamespaceMap.get(n);
return true;
default:
return true;
}
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case CLASS:
visitClass(t, n, parent);
break;
case INTERFACE:
visitInterface(t, n, parent);
break;
case ENUM:
visitEnum(t, n, parent);
break;
case NAME:
case REST:
maybeVisitColonType(t, n, n);
break;
case FUNCTION:
visitFunction(t, n, parent);
break;
case TYPE_ALIAS:
visitTypeAlias(t, n, parent);
break;
case DECLARE:
visitAmbientDeclaration(t, n, parent);
break;
case EXPORT:
visitExport(t, n, parent);
break;
case NAMESPACE:
visitNamespaceDeclaration(t, n, parent);
break;
case VAR:
case LET:
case CONST:
visitVarInsideNamespace(t, n, parent);
break;
case SCRIPT:
break;
default:
}
if (NodeUtil.isStatementParent(n)) {
popOverloads();
}
}
private void visitNamespaceDeclaration(NodeTraversal t, Node n, Node parent) {
popNamespace(n, parent);
for (Node name = NodeUtil.getRootOfQualifiedName(n.getFirstChild()); name != n;
name = name.getParent()) {
String fullName = maybePrependCurrNamespace(name.getQualifiedName());
if (!convertedNamespaces.contains(fullName)) {
JSDocInfoBuilder doc = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
doc.recordConstancy();
Node namespaceDec = NodeUtil.newQNameDeclaration(
compiler, fullName, IR.objectlit(), doc.build()).useSourceInfoFromForTree(n);
parent.addChildBefore(namespaceDec, n);
convertedNamespaces.add(fullName);
}
}
replaceWithNodes(t, n, n.getLastChild().children());
}
private void maybeAddGenerics(Node n, Node jsDocNode) {
Node name = n.getFirstChild();
Node generics = (Node) name.getProp(Node.GENERIC_TYPE_LIST);
if (generics != null) {
JSDocInfoBuilder doc = JSDocInfoBuilder.maybeCopyFrom(jsDocNode.getJSDocInfo());
// Discard the type bound (the "extends" part) for now
for (Node typeName : generics.children()) {
doc.recordTemplateTypeName(typeName.getString());
if (typeName.hasChildren()) {
compiler.report(JSError.make(name, CANNOT_CONVERT_BOUNDED_GENERICS));
typeName.removeChildren();
}
}
name.removeProp(Node.GENERIC_TYPE_LIST);
jsDocNode.setJSDocInfo(doc.build());
}
}
private void visitClass(NodeTraversal t, Node n, Node parent) {
maybeAddGenerics(n, n);
JSDocInfoBuilder doc = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
Node interfaces = (Node) n.getProp(Node.IMPLEMENTS);
if (interfaces != null) {
for (Node child : interfaces.children()) {
Node type = convertWithLocation(child);
doc.recordImplementedInterface(new JSTypeExpression(type, n.getSourceFileName()));
}
n.removeProp(Node.IMPLEMENTS);
}
Node superType = n.getSecondChild();
Node newSuperType = maybeGetQualifiedNameNode(superType);
if (newSuperType != superType) {
n.replaceChild(superType, newSuperType);
}
Node classMembers = n.getLastChild();
ClassDeclarationMetadata metadata = ClassDeclarationMetadata.create(n, parent);
for (Node member : classMembers.children()) {
if (member.isCallSignature()) {
compiler.report(JSError.make(n, CALL_SIGNATURE_NOT_SUPPORTED));
continue;
}
if (member.isIndexSignature()) {
doc.recordImplementedInterface(createIObject(t, member));
continue;
}
// Functions are handled by the regular Es6ToEs3Converter
if (!member.isMemberVariableDef() && !member.getBooleanProp(Node.COMPUTED_PROP_VARIABLE)) {
maybeAddVisibility(member);
continue;
}
if (metadata == null) {
compiler.report(JSError.make(n, CANNOT_CONVERT_MEMBER_VARIABLES));
return;
}
metadata.insertNodeAndAdvance(createPropertyDefinition(t, member, metadata.fullClassName));
t.reportCodeChange();
}
n.setJSDocInfo(doc.build());
maybeCreateQualifiedDeclaration(t, n, parent);
}
private void visitInterface(NodeTraversal t, Node n, Node parent) {
maybeAddGenerics(n, n);
Node name = n.getFirstChild();
Node superTypes = name.getNext();
JSDocInfoBuilder doc = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
doc.recordInterface();
if (!superTypes.isEmpty()) {
for (Node child : superTypes.children()) {
Node type = convertWithLocation(child);
doc.recordExtendedInterface(new JSTypeExpression(type, n.getSourceFileName()));
}
}
Node insertionPoint = n;
Node members = n.getLastChild();
for (Node member : members.children()) {
if (member.isCallSignature()) {
compiler.report(JSError.make(n, CALL_SIGNATURE_NOT_SUPPORTED));
}
if (member.isIndexSignature()) {
doc.recordExtendedInterface(createIObject(t, member));
}
// Synthesize a block for method signatures, or convert it to a member variable if optional.
if (member.isMemberFunctionDef()) {
Node function = member.getFirstChild();
if (function.isOptionalEs6Typed()) {
member = convertMemberFunctionToMemberVariable(member);
} else {
function.getLastChild().setToken(Token.BLOCK);
}
}
if (member.isMemberVariableDef()) {
Node newNode = createPropertyDefinition(t, member, name.getString());
insertionPoint.getParent().addChildAfter(newNode, insertionPoint);
insertionPoint = newNode;
}
}
n.setJSDocInfo(doc.build());
// Convert interface to class
n.setToken(Token.CLASS);
Node empty = new Node(Token.EMPTY).useSourceInfoIfMissingFrom(n);
n.replaceChild(superTypes, empty);
members.setToken(Token.CLASS_MEMBERS);
maybeCreateQualifiedDeclaration(t, n, parent);
t.reportCodeChange();
}
private JSTypeExpression createIObject(NodeTraversal t, Node indexSignature) {
Node indexType = convertWithLocation(indexSignature.getFirstChild()
.getDeclaredTypeExpression());
Node declaredType = convertWithLocation(indexSignature.getDeclaredTypeExpression());
Node block = new Node(Token.BLOCK, indexType, declaredType);
Node iObject = IR.string("IObject");
iObject.addChildToFront(block);
JSTypeExpression bang = new JSTypeExpression(new Node(Token.BANG, iObject)
.useSourceInfoIfMissingFromForTree(indexSignature), indexSignature.getSourceFileName());
indexSignature.detach();
t.reportCodeChange();
return bang;
}
private Node createPropertyDefinition(NodeTraversal t, Node member, String className) {
member.detach();
className = maybePrependCurrNamespace(className);
Node nameAccess = NodeUtil.newQName(compiler, className);
Node prototypeAccess = NodeUtil.newPropertyAccess(compiler, nameAccess, "prototype");
Node qualifiedMemberAccess = getQualifiedMemberAccess(
compiler, member, nameAccess, prototypeAccess);
// Copy type information.
maybeVisitColonType(t, member, member);
maybeAddVisibility(member);
qualifiedMemberAccess.setJSDocInfo(member.getJSDocInfo());
Node newNode = NodeUtil.newExpr(qualifiedMemberAccess);
return newNode.useSourceInfoIfMissingFromForTree(member);
}
/**
* Constructs a Node that represents an access to the given class member, qualified by either the
* static or the instance access context, depending on whether the member is static.
*
* <p><b>WARNING:</b> {@code member} may be modified/destroyed by this method, do not use it
* afterwards.
*/
private static Node getQualifiedMemberAccess(AbstractCompiler compiler, Node member,
Node staticAccess, Node instanceAccess) {
Node context = member.isStaticMember() ? staticAccess : instanceAccess;
context = context.cloneTree();
if (member.isComputedProp()) {
return IR.getelem(context, member.removeFirstChild());
} else {
return NodeUtil.newPropertyAccess(compiler, context, member.getString());
}
}
private void visitEnum(NodeTraversal t, Node n, Node parent) {
Node name = n.getFirstChild();
Node members = n.getLastChild();
double nextValue = 0;
Node[] stringKeys = new Node[members.getChildCount()];
int i = 0;
for (Node child = members.getFirstChild(); child != null; child = child.getNext(), i++) {
if (child.hasChildren()) {
nextValue = child.getFirstChild().getDouble() + 1;
} else {
child.addChildToFront(IR.number(nextValue++));
}
stringKeys[i] = child;
}
members.detachChildren();
String oldName = name.getString();
String qName = maybePrependCurrNamespace(oldName);
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
builder.recordEnumParameterType(
new JSTypeExpression(IR.string("number"), n.getSourceFileName()));
Node newDec = NodeUtil.newQNameDeclaration(
compiler,
qName,
IR.objectlit(stringKeys),
builder.build()).useSourceInfoFromForTree(n);
n.setJSDocInfo(null);
parent.replaceChild(n, newDec);
t.reportCodeChange();
}
private void visitFunction(NodeTraversal t, Node n, Node parent) {
// For member functions (eg. class Foo<T> { f() {} }), the JSDocInfo
// needs to go on the synthetic MEMBER_FUNCTION_DEF node.
boolean isMemberFunctionDef = parent.isMemberFunctionDef();
// Currently, we remove the overloading signature and drop the type information on the original
// signature.
String name = isMemberFunctionDef ? parent.getString() : n.getFirstChild().getString();
if (!name.isEmpty() && overloadStack.peek().containsKey(name)) {
compiler.report(JSError.make(n, OVERLOAD_NOT_SUPPORTED));
if (isMemberFunctionDef) {
parent.detach();
} else {
n.detach();
}
if (!processedOverloads.contains(overloadStack)) {
Node original = overloadStack.peek().get(name);
processedOverloads.add(original);
Node paramList = original.getSecondChild();
paramList.removeChildren();
Node originalParent = original.getParent();
Node originalJsDocNode = originalParent.isMemberFunctionDef() || originalParent.isAssign()
? originalParent : original;
JSDocInfoBuilder builder = new JSDocInfoBuilder(false);
builder.recordType(new JSTypeExpression(
convertWithLocation(TypeDeclarationsIR.namedType("Function")), n.getSourceFileName()));
originalJsDocNode.setJSDocInfo(builder.build());
}
t.reportCodeChange();
return;
}
overloadStack.peek().put(name, n);
Node jsDocNode = isMemberFunctionDef ? parent : n;
maybeAddGenerics(n, jsDocNode);
// Return types are colon types on the function node. Optional member functions are handled
// separately.
if (!(isMemberFunctionDef && n.isOptionalEs6Typed())) {
maybeVisitColonType(t, n, jsDocNode);
}
if (n.getLastChild().isEmpty()) {
n.replaceChild(n.getLastChild(), IR.block().useSourceInfoFrom(n));
}
if (!isMemberFunctionDef) {
maybeCreateQualifiedDeclaration(t, n, parent);
}
}
private void maybeAddVisibility(Node n) {
Visibility access = (Visibility) n.getProp(Node.ACCESS_MODIFIER);
if (access != null) {
if (n.isComputedProp()) {
compiler.report(JSError.make(n, COMPUTED_PROP_ACCESS_MODIFIER));
}
JSDocInfoBuilder memberDoc = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
memberDoc.recordVisibility(access);
n.setJSDocInfo(memberDoc.build());
n.removeProp(Node.ACCESS_MODIFIER);
}
}
private void maybeVisitColonType(NodeTraversal t, Node n, Node jsDocNode) {
Node type = n.getDeclaredTypeExpression();
boolean hasColonType = type != null;
if (n.isRest() && hasColonType) {
type = new Node(Token.ELLIPSIS, convertWithLocation(type.removeFirstChild()));
} else if (n.isMemberVariableDef()) {
if (type != null) {
type = maybeProcessOptionalProperty(n, type);
}
} else {
type = maybeProcessOptionalParameter(n, type);
}
if (type == null) {
return;
}
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(jsDocNode.getJSDocInfo());
JSTypeExpression typeExpression = new JSTypeExpression(type, n.getSourceFileName());
switch (n.getToken()) {
case FUNCTION:
builder.recordReturnType(typeExpression);
break;
case MEMBER_VARIABLE_DEF:
builder.recordType(typeExpression);
break;
default:
builder.recordType(typeExpression);
builder.recordInlineType();
}
jsDocNode.setJSDocInfo(builder.build());
if (hasColonType) {
n.setDeclaredTypeExpression(null);
t.reportCodeChange();
}
}
private void visitTypeAlias(NodeTraversal t, Node n, Node parent) {
String alias = n.getString();
if (t.getScope().isDeclared(alias, true)) {
compiler.report(
JSError.make(n, TYPE_ALIAS_ALREADY_DECLARED, alias));
}
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(n.getJSDocInfo());
builder.recordTypedef(new JSTypeExpression(
convertWithLocation(n.getFirstChild()), n.getSourceFileName()));
Node newName =
maybeGetQualifiedNameNode(IR.name(n.getString())).useSourceInfoIfMissingFromForTree(n);
Node newDec1 = NodeUtil.newQNameDeclaration(
compiler,
newName.getQualifiedName(),
null,
builder.build()).useSourceInfoFromForTree(n);
parent.replaceChild(n, newDec1);
t.reportCodeChange();
}
private void visitAmbientDeclaration(NodeTraversal t, Node n, Node parent) {
if (!n.isFromExterns()) {
compiler.report(JSError.make(n, DECLARE_IN_NON_EXTERNS));
}
Node insertionPoint = n;
Node topLevel = parent;
boolean insideExport = parent.isExport();
if (insideExport) {
insertionPoint = parent;
topLevel = parent.getParent();
}
// The node can have multiple children if transformed from an ambient namespace declaration.
for (Node c : n.children()) {
if (c.isConst()) {
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(c.getJSDocInfo());
builder.recordConstancy();
c.setToken(Token.VAR);
c.setJSDocInfo(builder.build());
}
Node toAdd = c.detach();
if (insideExport && !toAdd.isExprResult()) {
// We want to keep the "export" declaration in externs
toAdd = new Node(Token.EXPORT, toAdd).srcref(parent);
}
topLevel.addChildBefore(toAdd, insertionPoint);
}
insertionPoint.detach();
t.reportCodeChange();
}
private void visitExport(NodeTraversal t, Node n, Node parent) {
if (currNamespace != null) {
replaceWithNodes(t, n, n.children());
} else if (n.hasMoreThanOneChild()) {
Node insertPoint = n;
for (Node c = n.getSecondChild(); c != null; c = c.getNext()) {
Node toAdd;
if (!c.isExprResult()) {
toAdd = n.cloneNode();
toAdd.addChildToFront(c.detach());
} else {
toAdd = c.detach();
}
parent.addChildAfter(toAdd, insertPoint);
insertPoint = toAdd;
}
t.reportCodeChange();
}
}
private void replaceWithNodes(NodeTraversal t, Node n, Iterable<Node> replacements) {
Node insertPoint = n;
for (Node c : replacements) {
Node detached = c.detach();
n.getParent().addChildAfter(detached, insertPoint);
insertPoint = detached;
}
n.detach();
t.reportCodeChange();
}
private void visitVarInsideNamespace(NodeTraversal t, Node n, Node parent) {
if (currNamespace != null) {
Node insertPoint = n;
for (Node child : n.children()) {
Node name = child;
String oldName = name.getString();
String qName = maybePrependCurrNamespace(oldName);
JSDocInfoBuilder builder = JSDocInfoBuilder.maybeCopyFrom(child.getJSDocInfo());
if (n.isConst()) {
builder.recordConstancy();
}
Node newDec = NodeUtil.newQNameDeclaration(
compiler,
qName,
child.removeFirstChild(),
builder.build()).useSourceInfoFromForTree(n);
parent.addChildAfter(newDec, insertPoint);
insertPoint = newDec;
}
n.detach();
t.reportCodeChange();
}
}
private Node maybeCreateAnyType(Node n, Node type) {
return type == null ? TypeDeclarationsIR.anyType().useSourceInfoIfMissingFrom(n) : type;
}
private Node maybeProcessOptionalParameter(Node n, Node type) {
if (n.isOptionalEs6Typed()) {
n.putBooleanProp(Node.OPT_ES6_TYPED, false);
type = maybeCreateAnyType(n, type);
return new Node(Token.EQUALS, convertWithLocation(type));
} else {
return type == null ? null : convertWithLocation(type);
}
}
private Node maybeProcessOptionalProperty(Node n, Node type) {
if (n.isOptionalEs6Typed()) {
n.putBooleanProp(Node.OPT_ES6_TYPED, false);
TypeDeclarationNode baseType = (TypeDeclarationNode) maybeCreateAnyType(n, type);
type = TypeDeclarationsIR.unionType(
ImmutableList.of(baseType, TypeDeclarationsIR.undefinedType()));
type.useSourceInfoIfMissingFromForTree(baseType);
} else {
type = maybeCreateAnyType(n, type);
}
return convertWithLocation(type);
}
private Node convertWithLocation(Node type) {
return convertDeclaredTypeToJSDoc(type).useSourceInfoIfMissingFrom(type);
}
private Node convertDeclaredTypeToJSDoc(Node type) {
Preconditions.checkArgument(type instanceof TypeDeclarationNode);
switch (type.getToken()) {
// "Primitive" types.
case STRING_TYPE:
return IR.string("string");
case BOOLEAN_TYPE:
return IR.string("boolean");
case NUMBER_TYPE:
return IR.string("number");
case VOID_TYPE:
return IR.string("void");
case UNDEFINED_TYPE:
return IR.string("undefined");
case ANY_TYPE:
return new Node(Token.QMARK);
// Named types.
case NAMED_TYPE:
return convertNamedType(type);
case ARRAY_TYPE: {
Node arrayType = IR.string("Array");
Node memberType = convertWithLocation(type.getFirstChild());
arrayType.addChildToFront(
new Node(Token.BLOCK, memberType).useSourceInfoIfMissingFrom(type));
return new Node(Token.BANG, arrayType);
}
case PARAMETERIZED_TYPE: {
Node namedType = type.getFirstChild();
Node result = convertWithLocation(namedType);
Node typeParameterTarget =
result.getToken() == Token.BANG ? result.getFirstChild() : result;
Node parameters = IR.block().useSourceInfoIfMissingFrom(type);
typeParameterTarget.addChildToFront(parameters);
for (Node param = namedType.getNext(); param != null; param = param.getNext()) {
parameters.addChildToBack(convertWithLocation(param));
}
return result;
}
// Composite types.
case FUNCTION_TYPE: {
Node returnType = type.getFirstChild();
Node paramList = new Node(Token.PARAM_LIST);
for (Node param = returnType.getNext(); param != null; param = param.getNext()) {
Node paramType = param.getDeclaredTypeExpression();
if (param.isRest()) {
if (paramType == null) {
paramType = new Node(Token.ELLIPSIS, new Node(Token.QMARK));
} else {
paramType = new Node(Token.ELLIPSIS,
convertWithLocation(paramType.getFirstChild()));
}
} else {
paramType = maybeProcessOptionalParameter(param,
maybeCreateAnyType(param, paramType));
}
paramList.addChildToBack(paramType);
}
Node function = new Node(Token.FUNCTION);
// TODO(moz): We should always add a PARAM_LIST in JsDocInfoParser
if (paramList.hasChildren()) {
function.addChildToBack(paramList);
}
function.addChildToBack(convertWithLocation(returnType));
return function;
}
case UNION_TYPE:
Node pipe = new Node(Token.PIPE);
for (Node child : type.children()) {
pipe.addChildToBack(convertWithLocation(child));
}
return pipe;
case RECORD_TYPE: {
Node lb = new Node(Token.LB);
for (Node member : type.children()) {
if (member.isMemberFunctionDef()) {
member = convertMemberFunctionToMemberVariable(member);
} else if (!member.isMemberVariableDef()) {
compiler.report(JSError.make(type, UNSUPPORTED_RECORD_TYPE));
continue;
}
Node colon = new Node(Token.COLON);
member.setToken(Token.STRING_KEY);
Node memberType =
maybeProcessOptionalProperty(member, member.getDeclaredTypeExpression());
member.setDeclaredTypeExpression(null);
colon.addChildToBack(member.detach());
colon.addChildToBack(memberType);
lb.addChildToBack(colon);
}
return new Node(Token.LC, lb);
}
case TYPEOF:
// Currently, TypeQuery is not supported in Closure's type system.
compiler.report(JSError.make(type, TYPE_QUERY_NOT_SUPPORTED));
return new Node(Token.QMARK);
case STRING:
compiler.report(JSError.make(type, SPECIALIZED_SIGNATURE_NOT_SUPPORTED));
return new Node(Token.QMARK);
default:
// TODO(moz): Implement.
break;
}
throw new IllegalArgumentException(
"Unexpected node type for type conversion: " + type.getToken());
}
private Node convertNamedType(Node type) {
Node oldNameNode = type.getFirstChild();
Node newNameNode = maybeGetQualifiedNameNode(oldNameNode);
if (newNameNode != oldNameNode) {
type.replaceChild(oldNameNode, newNameNode);
}
Node propTree = type.getFirstChild();
String dotted = propTree.getQualifiedName();
// In the native type syntax, nominal types are non-nullable by default.
// NOTE(dimvar): This adds ! in front of type variables as well.
// Minor issue, not worth fixing for now.
// To fix, we must first transpile declarations of generic types, collect
// the type variables in scope, and use them during transpilation.
return new Node(Token.BANG, IR.string(dotted));
}
private void maybeCreateQualifiedDeclaration(NodeTraversal t, Node n, Node parent) {
if (currNamespace != null) {
Node name = n.getFirstChild();
String oldName = name.getString();
String qName = maybePrependCurrNamespace(oldName);
Node newName = n.isFunction() ? IR.name("") : IR.empty();
newName.useSourceInfoFrom(n);
n.replaceChild(name, newName);
Node placeHolder = IR.empty();
parent.replaceChild(n, placeHolder);
Node newDec = NodeUtil.newQNameDeclaration(
compiler,
qName,
n,
n.getJSDocInfo()).useSourceInfoFromForTree(n);
n.setJSDocInfo(null);
parent.replaceChild(placeHolder, newDec);
t.reportCodeChange();
}
}
private Node convertMemberFunctionToMemberVariable(Node member) {
Node function = member.getFirstChild();
Node memberVariable = Node.newString(Token.MEMBER_VARIABLE_DEF, member.getString());
memberVariable.useSourceInfoFrom(member);
if (!processedOverloads.contains(function)) {
Node returnType = maybeCreateAnyType(function, function.getDeclaredTypeExpression());
LinkedHashMap<String, TypeDeclarationNode> required = new LinkedHashMap<>();
LinkedHashMap<String, TypeDeclarationNode> optional = new LinkedHashMap<>();
String restName = null;
TypeDeclarationNode restType = null;
for (Node param : function.getSecondChild().children()) {
if (param.isName()) {
if (param.isOptionalEs6Typed()) {
optional.put(param.getString(), param.getDeclaredTypeExpression());
} else {
required.put(param.getString(), param.getDeclaredTypeExpression());
}
} else if (param.isRest()) {
restName = param.getFirstChild().getString();
restType = param.getDeclaredTypeExpression();
}
}
TypeDeclarationNode type =
TypeDeclarationsIR.functionType(returnType, required, optional, restName, restType);
memberVariable.setDeclaredTypeExpression(type);
} else {
memberVariable.setDeclaredTypeExpression(TypeDeclarationsIR.namedType("Function"));
}
memberVariable.putBooleanProp(Node.OPT_ES6_TYPED, function.isOptionalEs6Typed());
member.replaceWith(memberVariable);
return memberVariable;
}
private Node maybeGetQualifiedNameNode(Node oldNameNode) {
if (oldNameNode.isName()) {
String oldName = oldNameNode.getString();
for (Namespace definitionNamespace = currNamespace; definitionNamespace != null;
definitionNamespace = definitionNamespace.parent) {
if (definitionNamespace.typeNames.contains(oldName)) {
return NodeUtil.newQName(compiler, definitionNamespace.name + "." + oldName)
.useSourceInfoFromForTree(oldNameNode);
}
}
}
return oldNameNode;
}
private void pushOverloads() {
overloadStack.push(new HashMap<String, Node>());
}
private void popOverloads() {
overloadStack.pop();
}
private String maybePrependCurrNamespace(String oldName) {
return currNamespace == null ? oldName : currNamespace.name + "." + oldName;
}
private void popNamespace(Node n, Node parent) {
if (n.getToken() == Token.NAMESPACE) {
Node parentModuleRoot;
Node grandParent = parent.getParent();
switch (parent.getToken()) {
case DECLARE:
case EXPORT:
if (parent.getParent().isExport()) {
parentModuleRoot = grandParent.getGrandparent();
} else {
parentModuleRoot = grandParent.getParent();
}
break;
default:
parentModuleRoot = grandParent;
}
currNamespace = nodeNamespaceMap.get(parentModuleRoot);
}
}
private class ScanNamespaces implements NodeTraversal.Callback {
private Map<String, Namespace> namespaces = new HashMap<>();
@Override
public boolean shouldTraverse(NodeTraversal t, Node n, Node parent) {
switch (n.getToken()) {
case ROOT:
case SCRIPT:
case NAMESPACE_ELEMENTS:
return true;
case DECLARE:
return n.getFirstChild().getToken() == Token.NAMESPACE;
case EXPORT:
switch (n.getFirstChild().getToken()) {
case CLASS:
case INTERFACE:
case ENUM:
case TYPE_ALIAS:
case NAMESPACE:
case DECLARE:
return true;
default:
break;
}
return false;
case NAMESPACE:
String[] segments = n.getFirstChild().getQualifiedName().split("\\.");
for (String s : segments) {
String currName = maybePrependCurrNamespace(s);
if (!namespaces.containsKey(currName)) {
currNamespace = new Namespace(currName, currNamespace);
namespaces.put(currName, currNamespace);
}
currNamespace = namespaces.get(currName);
}
nodeNamespaceMap.put(n, currNamespace);
return true;
case CLASS:
case INTERFACE:
case ENUM:
if (currNamespace != null) {
currNamespace.typeNames.add(n.getFirstChild().getString());
}
return true;
case TYPE_ALIAS:
if (currNamespace != null) {
currNamespace.typeNames.add(n.getString());
}
return true;
default:
break;
}
return false;
}
@Override
public void visit(NodeTraversal t, Node n, Node parent) {
popNamespace(n, parent);
}
}
private static class Namespace {
private final String name;
private Set<String> typeNames;
private Namespace parent;
private Namespace(String name, Namespace parent) {
this.name = name;
this.parent = parent;
this.typeNames = new HashSet<>();
}
}
}