com.google.javascript.jscomp.AstFactory Maven / Gradle / Ivy
Show all versions of closure-compiler Show documentation
/*
* Copyright 2018 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.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.javascript.rhino.IR;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.Token;
import com.google.javascript.rhino.jstype.BooleanLiteralSet;
import com.google.javascript.rhino.jstype.FunctionType;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.JSTypeNative;
import com.google.javascript.rhino.jstype.JSTypeRegistry;
import com.google.javascript.rhino.jstype.ObjectType;
import com.google.javascript.rhino.jstype.TemplateTypeMap;
import com.google.javascript.rhino.jstype.TemplateTypeReplacer;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
/**
* Creates AST nodes and subtrees.
*
* This class supports creating nodes either with or without type information.
*
*
The idea is that client code can create the trees of nodes it needs without having to contain
* logic for deciding whether type information should be added or not, and only minimal logic for
* determining which types to add when they are necessary. Most methods in this class are able to
* determine the correct type information from already existing AST nodes and the current scope.
*/
final class AstFactory {
private static final Splitter DOT_SPLITTER = Splitter.on(".");
@Nullable private final JSTypeRegistry registry;
// We need the unknown type so frequently, it's worth caching it.
private final JSType unknownType;
// We might not need Arguments type, but if we do, we should avoid redundant lookups
private final Supplier argumentsTypeSupplier;
private AstFactory() {
this.registry = null;
unknownType = null;
argumentsTypeSupplier = () -> null;
}
private AstFactory(JSTypeRegistry registry) {
this.registry = registry;
this.unknownType = getNativeType(JSTypeNative.UNKNOWN_TYPE);
this.argumentsTypeSupplier =
Suppliers.memoize(
() -> {
JSType globalType = registry.getGlobalType("Arguments");
if (globalType != null) {
return globalType;
} else {
return unknownType;
}
});
;
}
static AstFactory createFactoryWithoutTypes() {
return new AstFactory();
}
static AstFactory createFactoryWithTypes(JSTypeRegistry registry) {
return new AstFactory(registry);
}
/** Does this class instance add types to the nodes it creates? */
boolean isAddingTypes() {
return registry != null;
}
/**
* Returns a new EXPR_RESULT node.
*
* Statements have no type information, so this is functionally the same as calling {@code
* IR.exprResult(expr)}. It exists so that a pass can be consistent about always using {@code
* AstFactory} to create new nodes.
*/
Node exprResult(Node expr) {
return IR.exprResult(expr).srcref(expr);
}
/**
* Returns a new EMPTY node.
*
*
EMPTY Nodes have no type information, so this is functionally the same as calling {@code
* IR.empty()}. It exists so that a pass can be consistent about always using {@code AstFactory}
* to create new nodes.
*/
Node createEmpty() {
return IR.empty();
}
/**
* Returns a new BLOCK node.
*
*
Blocks have no type information, so this is functionally the same as calling {@code
* IR.block(statements)}. It exists so that a pass can be consistent about always using {@code
* AstFactory} to create new nodes.
*/
Node createBlock(Node... statements) {
return IR.block(statements);
}
/**
* Returns a new IF node.
*
*
Blocks have no type information, so this is functionally the same as calling {@code
* IR.ifNode(cond, then)}. It exists so that a pass can be consistent about always using {@code
* AstFactory} to create new nodes.
*/
Node createIf(Node cond, Node then) {
return IR.ifNode(cond, then);
}
/**
* Returns a new IF node.
*
*
Blocks have no type information, so this is functionally the same as calling {@code
* IR.ifNode(cond, then, elseNode)}. It exists so that a pass can be consistent about always using
* {@code AstFactory} to create new nodes.
*/
Node createIf(Node cond, Node then, Node elseNode) {
return IR.ifNode(cond, then, elseNode);
}
/**
* Returns a new FOR node.
*
*
Blocks have no type information, so this is functionally the same as calling {@code
* IR.forNode(init, cond, incr, body)}. It exists so that a pass can be consistent about always
* using {@code AstFactory} to create new nodes.
*/
Node createFor(Node init, Node cond, Node incr, Node body) {
return IR.forNode(init, cond, incr, body);
}
/**
* Returns a new BREAK node.
*
*
Breaks have no type information, so this is functionally the same as calling {@code
* IR.breakNode()}. It exists so that a pass can be consistent about always using {@code
* AstFactory} to create new nodes.
*/
Node createBreak() {
return IR.breakNode();
}
/**
* Returns a new {@code return} statement.
*
*
Return statements have no type information, so this is functionally the same as calling
* {@code IR.return(value)}. It exists so that a pass can be consistent about always using {@code
* AstFactory} to create new nodes.
*/
Node createReturn(Node value) {
return IR.returnNode(value);
}
/**
* Returns a new {@code yield} expression.
*
* @param jsType Type we expect to get back after the yield
* @param value value to yield
*/
Node createYield(JSType jsType, Node value) {
Node result = IR.yield(value);
if (isAddingTypes()) {
result.setJSType(checkNotNull(jsType));
}
return result;
}
/**
* Returns a new {@code await} expression.
*
* @param jsType Type we expect to get back after the await
* @param value value to await
*/
Node createAwait(JSType jsType, Node value) {
Node result = IR.await(value);
if (isAddingTypes()) {
result.setJSType(checkNotNull(jsType));
}
return result;
}
Node createString(String value) {
Node result = IR.string(value);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.STRING_TYPE));
}
return result;
}
Node createNumber(double value) {
Node result = IR.number(value);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.NUMBER_TYPE));
}
return result;
}
Node createBoolean(boolean value) {
Node result = value ? IR.trueNode() : IR.falseNode();
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createNull() {
Node result = IR.nullNode();
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.NULL_TYPE));
}
return result;
}
Node createVoid(Node child) {
Node result = IR.voidNode(child);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.VOID_TYPE));
}
return result;
}
/** Returns a new Node representing the undefined value. */
public Node createUndefinedValue() {
// We prefer `void 0` as being shorter than `undefined`.
// Also, it's technically possible for malicious code to assign a value to `undefined`.
return createVoid(createNumber(0));
}
Node createCastToUnknown(Node child, JSDocInfo jsdoc) {
Node result = IR.cast(child, jsdoc);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.UNKNOWN_TYPE));
}
return result;
}
Node createNot(Node child) {
Node result = IR.not(child);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createThis(JSType thisType) {
Node result = IR.thisNode();
if (isAddingTypes()) {
result.setJSType(checkNotNull(thisType));
}
return result;
}
Node createSuper(JSType superType) {
Node result = IR.superNode();
if (isAddingTypes()) {
result.setJSType(checkNotNull(superType));
}
return result;
}
/** Creates a THIS node with the correct type for the given function node. */
Node createThisForFunction(Node functionNode) {
final Node result = IR.thisNode();
if (isAddingTypes()) {
result.setJSType(getTypeOfThisForFunctionNode(functionNode));
}
return result;
}
/** Creates a SUPER node with the correct type for the given function node. */
Node createSuperForFunction(Node functionNode) {
final Node result = IR.superNode();
if (isAddingTypes()) {
result.setJSType(getTypeOfSuperForFunctionNode(functionNode));
}
return result;
}
@Nullable
private JSType getTypeOfThisForFunctionNode(Node functionNode) {
if (isAddingTypes()) {
FunctionType functionType = getFunctionType(functionNode);
return checkNotNull(functionType.getTypeOfThis(), functionType);
} else {
return null; // not adding type information
}
}
@Nullable
private JSType getTypeOfSuperForFunctionNode(Node functionNode) {
if (isAddingTypes()) {
ObjectType thisType = getTypeOfThisForFunctionNode(functionNode).assertObjectType();
return checkNotNull(thisType.getSuperClassConstructor().getInstanceType(), thisType);
} else {
return null; // not adding type information
}
}
private FunctionType getFunctionType(Node functionNode) {
checkState(functionNode.isFunction(), "not a function: %s", functionNode);
// If the function declaration was cast to a different type, we want the original type
// from before the cast.
final JSType typeBeforeCast = functionNode.getJSTypeBeforeCast();
final FunctionType functionType;
if (typeBeforeCast != null) {
functionType = typeBeforeCast.assertFunctionType();
} else {
functionType = functionNode.getJSTypeRequired().assertFunctionType();
}
return functionType;
}
/** Creates a NAME node having the type of "this" appropriate for the given function node. */
Node createThisAliasReferenceForFunction(String aliasName, Node functionNode) {
final Node result = IR.name(aliasName);
if (isAddingTypes()) {
result.setJSType(getTypeOfThisForFunctionNode(functionNode));
}
return result;
}
/**
* Creates a statement declaring a const alias for "this" to be used in the given function node.
*
*
e.g. `const aliasName = this;`
*/
Node createThisAliasDeclarationForFunction(String aliasName, Node functionNode) {
return createSingleConstNameDeclaration(
aliasName, createThis(getTypeOfThisForFunctionNode(functionNode)));
}
/**
* Creates a new `let` declaration for a single variable name with a void type and no JSDoc.
*
*
e.g. `let variableName`
*/
Node createSingleLetNameDeclaration(String variableName) {
return IR.let(createName(variableName, JSTypeNative.VOID_TYPE));
}
/**
* Creates a new `var` declaration statement for a single variable name with void type and no
* JSDoc.
*
*
e.g. `var variableName`
*/
Node createSingleVarNameDeclaration(String variableName) {
return IR.var(createName(variableName, JSTypeNative.VOID_TYPE));
}
/**
* Creates a new `var` declaration statement for a single variable name.
*
*
Takes the type for the variable name from the value node.
*
*
e.g. `var variableName = value;`
*/
Node createSingleVarNameDeclaration(String variableName, Node value) {
return IR.var(createName(variableName, value.getJSType()), value);
}
/**
* Creates a new `const` declaration statement for a single variable name.
*
*
Takes the type for the variable name from the value node.
*
*
e.g. `const variableName = value;`
*/
Node createSingleConstNameDeclaration(String variableName, Node value) {
return IR.constNode(createName(variableName, value.getJSType()), value);
}
/**
* Creates a reference to "arguments" with the type specified in externs, or unknown if the
* externs for it weren't included.
*/
Node createArgumentsReference() {
Node result = IR.name("arguments");
if (isAddingTypes()) {
result.setJSType(argumentsTypeSupplier.get());
}
return result;
}
/**
* Creates a statement declaring a const alias for "arguments".
*
*
e.g. `const argsAlias = arguments;`
*/
Node createArgumentsAliasDeclaration(String aliasName) {
return createSingleConstNameDeclaration(aliasName, createArgumentsReference());
}
Node createName(String name, JSType type) {
Node result = IR.name(name);
if (isAddingTypes()) {
result.setJSType(checkNotNull(type));
}
return result;
}
Node createName(String name, JSTypeNative nativeType) {
Node result = IR.name(name);
if (isAddingTypes()) {
result.setJSType(getNativeType(nativeType));
}
return result;
}
Node createNameWithUnknownType(String name) {
return createName(name, unknownType);
}
Node createName(Scope scope, String name) {
Node result = IR.name(name);
if (isAddingTypes()) {
result.setJSType(getVarNameType(scope, name));
}
return result;
}
Node createQName(Scope scope, String qname) {
return createQName(scope, DOT_SPLITTER.split(qname));
}
/**
* Looks up the type of a name from a {@link TypedScope} created from typechecking
*
* @param globalTypedScope Must be the top, global scope.
*/
Node createQName(TypedScope globalTypedScope, String qname) {
checkArgument(globalTypedScope == null || globalTypedScope.isGlobal(), globalTypedScope);
List nameParts = DOT_SPLITTER.splitToList(qname);
checkState(!nameParts.isEmpty());
String receiverPart = nameParts.get(0);
Node receiver = IR.name(receiverPart);
if (this.isAddingTypes()) {
TypedVar var = checkNotNull(globalTypedScope.getVar(receiverPart), receiverPart);
receiver.setJSType(checkNotNull(var.getType(), var));
}
List otherParts = nameParts.subList(1, nameParts.size());
return this.createGetProps(receiver, otherParts);
}
Node createQName(Scope scope, Iterable names) {
String baseName = checkNotNull(Iterables.getFirst(names, null));
Iterable propertyNames = Iterables.skip(names, 1);
return createQName(scope, baseName, propertyNames);
}
Node createQName(Scope scope, String baseName, String... propertyNames) {
checkNotNull(baseName);
return createQName(scope, baseName, Arrays.asList(propertyNames));
}
Node createQName(Scope scope, String baseName, Iterable propertyNames) {
Node baseNameNode = createName(scope, baseName);
return createGetProps(baseNameNode, propertyNames);
}
Node createGetProp(Node receiver, String propertyName) {
Node result = IR.getprop(receiver, createString(propertyName));
if (isAddingTypes()) {
result.setJSType(getJsTypeForProperty(receiver, propertyName));
}
return result;
}
/** Creates a tree of nodes representing `receiver.name1.name2.etc`. */
Node createGetProps(Node receiver, Iterable propertyNames) {
Node result = receiver;
for (String propertyName : propertyNames) {
result = createGetProp(result, propertyName);
}
return result;
}
/** Creates a tree of nodes representing `receiver.name1.name2.etc`. */
Node createGetProps(Node receiver, String firstPropName, String... otherPropNames) {
Node result = createGetProp(receiver, firstPropName);
for (String propertyName : otherPropNames) {
result = createGetProp(result, propertyName);
}
return result;
}
Node createGetElem(Node receiver, Node key) {
Node result = IR.getelem(receiver, key);
if (isAddingTypes()) {
// In general we cannot assume we know the type we get from a GETELEM.
// TODO(bradfordcsmith): When receiver is an Array or an Object, use the template
// type here.
result.setJSType(unknownType);
}
return result;
}
Node createDelProp(Node target) {
Node result = IR.delprop(target);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createStringKey(String key, Node value) {
Node result = IR.stringKey(key, value);
if (isAddingTypes()) {
result.setJSType(value.getJSType());
}
return result;
}
Node createComputedProperty(Node key, Node value) {
Node result = IR.computedProp(key, value);
if (isAddingTypes()) {
result.setJSType(value.getJSType());
}
return result;
}
/**
* Create a getter definition to be inserted into either a class body or object literal.
*
* {@code get name() { return value; }}
*/
Node createGetterDef(String name, Node value) {
JSType returnType = value.getJSType();
// Name is stored on the GETTER_DEF node. The function has no name.
Node functionNode =
createZeroArgFunction(/* name= */ "", IR.block(createReturn(value)), returnType);
Node getterNode = Node.newString(Token.GETTER_DEF, name);
getterNode.addChildToFront(functionNode);
return getterNode;
}
Node createIn(Node left, Node right) {
Node result = IR.in(left, right);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createComma(Node left, Node right) {
Node result = IR.comma(left, right);
if (isAddingTypes()) {
result.setJSType(right.getJSType());
}
return result;
}
Node createCommas(Node first, Node second, Node... rest) {
Node result = createComma(first, second);
for (Node next : rest) {
result = createComma(result, next);
}
return result;
}
Node createAnd(Node left, Node right) {
Node result = IR.and(left, right);
if (isAddingTypes()) {
JSType leftType = checkNotNull(left.getJSType(), left);
JSType rightType = checkNotNull(right.getJSType(), right);
BooleanLiteralSet possibleLhsBooleanValues = leftType.getPossibleToBooleanOutcomes();
switch (possibleLhsBooleanValues) {
case TRUE:
// left cannot be false, so rhs will always be evaluated
result.setJSType(rightType);
break;
case FALSE:
// left cannot be true, so rhs will never be evaluated
result.setJSType(leftType);
break;
case BOTH:
// result could be the type of either the lhs or the rhs
result.setJSType(leftType.getLeastSupertype(rightType));
break;
default:
checkState(
possibleLhsBooleanValues == BooleanLiteralSet.EMPTY,
"unexpected enum value: %s",
possibleLhsBooleanValues);
// TODO(bradfordcsmith): Should we be trying to determine whether we actually need
// NO_OBJECT_TYPE or similar here? It probably doesn't matter since this code is
// expected to execute only after all static type analysis has been done.
result.setJSType(getNativeType(JSTypeNative.NO_TYPE));
break;
}
}
return result;
}
Node createOr(Node left, Node right) {
Node result = IR.or(left, right);
if (isAddingTypes()) {
JSType leftType = checkNotNull(left.getJSType(), left);
JSType rightType = checkNotNull(right.getJSType(), right);
BooleanLiteralSet possibleLhsBooleanValues = leftType.getPossibleToBooleanOutcomes();
switch (possibleLhsBooleanValues) {
case TRUE:
// left cannot be false, so rhs will never be evaluated
result.setJSType(leftType);
break;
case FALSE:
// left cannot be true, so rhs will always be evaluated
result.setJSType(rightType);
break;
case BOTH:
// result could be the type of either the lhs or the rhs
result.setJSType(leftType.getLeastSupertype(rightType));
break;
default:
checkState(
possibleLhsBooleanValues == BooleanLiteralSet.EMPTY,
"unexpected enum value: %s",
possibleLhsBooleanValues);
// TODO(bradfordcsmith): Should we be trying to determine whether we actually need
// NO_OBJECT_TYPE or similar here? It probably doesn't matter since this code is
// expected to execute only after all static type analysis has been done.
result.setJSType(getNativeType(JSTypeNative.NO_TYPE));
break;
}
}
return result;
}
Node createCall(Node callee, Node... args) {
Node result = NodeUtil.newCallNode(callee, args);
if (isAddingTypes()) {
FunctionType calleeType = JSType.toMaybeFunctionType(callee.getJSType());
// TODO(sdh): this does not handle generic functions - we'd need to unify the argument types.
// checkState(calleeType == null || !calleeType.hasAnyTemplateTypes(), calleeType);
// TODO(bradfordcsmith): Consider throwing an exception if calleeType is null.
JSType returnType = calleeType != null ? calleeType.getReturnType() : unknownType;
result.setJSType(returnType);
}
return result;
}
/**
* Creates a call to Object.assign that returns the specified type.
*
*
Object.assign returns !Object in the externs, which can lose type information if the actual
* type is known.
*/
Node createObjectDotAssignCall(Scope scope, JSType returnType, Node... args) {
Node objAssign = createQName(scope, "Object", "assign");
Node result = createCall(objAssign, args);
if (isAddingTypes()) {
// Make a unique function type that returns the exact type we've inferred it to be.
// Object.assign in the externs just returns !Object, which loses type information.
JSType objAssignType =
registry.createFunctionTypeWithVarArgs(
returnType,
registry.getNativeType(JSTypeNative.OBJECT_TYPE),
registry.createUnionType(JSTypeNative.OBJECT_TYPE, JSTypeNative.NULL_TYPE));
objAssign.setJSType(objAssignType);
result.setJSType(returnType);
}
return result;
}
Node createNewNode(Node target, Node... args) {
Node result = IR.newNode(target, args);
if (isAddingTypes()) {
JSType instanceType = target.getJSType();
if (instanceType.isFunctionType()) {
instanceType = instanceType.toMaybeFunctionType().getInstanceType();
} else {
instanceType = getNativeType(JSTypeNative.UNKNOWN_TYPE);
}
result.setJSType(instanceType);
}
return result;
}
Node createObjectGetPrototypeOfCall(Node argObjectNode) {
Node objectName = createName("Object", JSTypeNative.OBJECT_FUNCTION_TYPE);
Node objectGetPrototypeOf = createGetProp(objectName, "getPrototypeOf");
Node result = createCall(objectGetPrototypeOf, argObjectNode);
if (isAddingTypes()) {
ObjectType typeOfArgObject = argObjectNode.getJSTypeRequired().assertObjectType();
JSType returnedType = getPrototypeObjectType(typeOfArgObject);
result.setJSType(returnedType);
// Return type of the function needs to match that of the entire expression. getPrototypeOf
// normally returns !Object.
objectGetPrototypeOf.setJSType(
registry.createFunctionType(returnedType, getNativeType(JSTypeNative.OBJECT_TYPE)));
}
return result;
}
ObjectType getPrototypeObjectType(ObjectType objectType) {
checkNotNull(objectType);
if (objectType.isUnknownType()) {
// Calling getImplicitPrototype() on the unknown type returns `null`, but we want
// the prototype of an unknown type to also be unknown.
// TODO(bradfordcsmith): Can we fix this behavior of the unknown type?
return objectType;
} else {
return checkNotNull(objectType.getImplicitPrototype(), "null prototype: %s", objectType);
}
}
/**
* Create a call that returns an instance of the given class type.
*
*
This method is intended for use in special cases, such as calling `super()` in a
* constructor.
*/
Node createConstructorCall(@Nullable JSType classType, Node callee, Node... args) {
Node result = NodeUtil.newCallNode(callee, args);
if (isAddingTypes()) {
checkNotNull(classType);
FunctionType constructorType = checkNotNull(classType.toMaybeFunctionType());
ObjectType instanceType = checkNotNull(constructorType.getInstanceType());
result.setJSType(instanceType);
}
return result;
}
/** Creates a statement `lhs = rhs;`. */
Node createAssignStatement(Node lhs, Node rhs) {
return exprResult(createAssign(lhs, rhs));
}
/** Creates an assignment expression `lhs = rhs` */
Node createAssign(Node lhs, Node rhs) {
Node result = IR.assign(lhs, rhs);
if (isAddingTypes()) {
result.setJSType(rhs.getJSType());
}
return result;
}
/** Creates an assignment expression `lhs = rhs` */
Node createAssign(String lhsName, Node rhs) {
return createAssign(createName(lhsName, rhs.getJSType()), rhs);
}
/**
* Creates an object-literal with zero or more elements, `{}`.
*
*
The type of the literal, if assigned, may be a supertype of the known properties.
*/
Node createObjectLit(Node... elements) {
Node result = IR.objectlit(elements);
if (isAddingTypes()) {
result.setJSType(registry.createAnonymousObjectType(null));
}
return result;
}
/** Creates an object-literal with zero or more elements and a specific type. */
Node createObjectLit(@Nullable JSType jsType, Node... elements) {
Node result = IR.objectlit(elements);
if (isAddingTypes()) {
result.setJSType(checkNotNull(jsType));
}
return result;
}
/** Creates an empty function `function() {}` */
Node createEmptyFunction(JSType type) {
Node result = NodeUtil.emptyFunction();
if (isAddingTypes()) {
checkNotNull(type);
checkArgument(type.isFunctionType(), type);
result.setJSType(checkNotNull(type));
}
return result;
}
/** Creates an empty function `function*() {}` */
Node createEmptyGeneratorFunction(JSType type) {
Node result = NodeUtil.emptyFunction();
result.setIsGeneratorFunction(true);
if (isAddingTypes()) {
checkNotNull(type);
checkArgument(type.isFunctionType(), type);
result.setJSType(checkNotNull(type));
}
return result;
}
/**
* Creates a function `function name(paramList) { body }`
*
* @param name STRING node - empty string if no name
* @param paramList PARAM_LIST node
* @param body BLOCK node
* @param type type to apply to the function itself
*/
Node createFunction(String name, Node paramList, Node body, JSType type) {
Node nameNode = createName(name, type);
Node result = IR.function(nameNode, paramList, body);
if (isAddingTypes()) {
checkArgument(type.isFunctionType(), type);
result.setJSType(type);
}
return result;
}
Node createZeroArgFunction(String name, Node body, @Nullable JSType returnType) {
FunctionType functionType =
isAddingTypes() ? registry.createFunctionType(returnType).toMaybeFunctionType() : null;
return createFunction(name, IR.paramList(), body, functionType);
}
Node createZeroArgGeneratorFunction(String name, Node body, @Nullable JSType returnType) {
Node result = createZeroArgFunction(name, body, returnType);
result.setIsGeneratorFunction(true);
return result;
}
Node createZeroArgArrowFunctionForExpression(Node expression) {
Node result = IR.arrowFunction(IR.name(""), IR.paramList(), expression);
if (isAddingTypes()) {
// It feels like we should be adding type-of-this here, but it should remain unknown,
// because you're allowed to supply any kind of value of `this` when calling an arrow
// function. It will just be ignored in favor of the `this` in the scope where the
// arrow was defined.
FunctionType functionType =
FunctionType.builder(registry)
.withReturnType(expression.getJSTypeRequired())
.withParameters()
.buildAndResolve();
result.setJSType(functionType);
}
return result;
}
Node createMemberFunctionDef(String name, Node function) {
// A function used for a member function definition must have an empty name,
// because the name string goes on the MEMBER_FUNCTION_DEF node.
checkArgument(function.getFirstChild().getString().isEmpty(), function);
Node result = IR.memberFunctionDef(name, function);
if (isAddingTypes()) {
// member function definition must share the type of the function that implements it
result.setJSType(function.getJSType());
}
return result;
}
Node createSheq(Node expr1, Node expr2) {
Node result = IR.sheq(expr1, expr2);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createEq(Node expr1, Node expr2) {
Node result = IR.eq(expr1, expr2);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createNe(Node expr1, Node expr2) {
Node result = IR.ne(expr1, expr2);
if (isAddingTypes()) {
result.setJSType(getNativeType(JSTypeNative.BOOLEAN_TYPE));
}
return result;
}
Node createHook(Node condition, Node expr1, Node expr2) {
Node result = IR.hook(condition, expr1, expr2);
if (isAddingTypes()) {
result.setJSType(registry.createUnionType(expr1.getJSType(), expr2.getJSType()));
}
return result;
}
Node createArraylit(Node... elements) {
return createArraylit(Arrays.asList(elements));
}
Node createArraylit(Iterable elements) {
Node result = IR.arraylit(elements);
if (isAddingTypes()) {
result.setJSType(
registry.createTemplatizedType(
registry.getNativeObjectType(JSTypeNative.ARRAY_TYPE),
// TODO(nickreid): Use a reasonable template type. Remeber to consider SPREAD.
getNativeType(JSTypeNative.UNKNOWN_TYPE)));
}
return result;
}
Node createJSCompMakeIteratorCall(Node iterable, Scope scope) {
String function = "makeIterator";
Node makeIteratorName = createQName(scope, "$jscomp", function);
// Since createCall (currently) doesn't handle templated functions, fill in the template types
// of makeIteratorName manually.
if (isAddingTypes() && !makeIteratorName.getJSType().isUnknownType()) {
// if makeIteratorName has the unknown type, we must have not injected the required runtime
// libraries - hopefully because this is in a test using NonInjectingCompiler.
// e.g get `number` from `Iterable`
JSType iterableType =
iterable
.getJSType()
.getTemplateTypeMap()
.getResolvedTemplateType(registry.getIterableTemplate());
JSType makeIteratorType = makeIteratorName.getJSType();
// e.g. replace
// function(Iterable): Iterator
// with
// function(Iterable): Iterator
makeIteratorName.setJSType(replaceTemplate(makeIteratorType, ImmutableList.of(iterableType)));
}
return createCall(makeIteratorName, iterable);
}
Node createJscompArrayFromIteratorCall(Node iterator, Scope scope) {
Node makeIteratorName = createQName(scope, "$jscomp", "arrayFromIterator");
// Since createCall (currently) doesn't handle templated functions, fill in the template types
// of makeIteratorName manually.
if (isAddingTypes() && !makeIteratorName.getJSType().isUnknownType()) {
// if makeIteratorName has the unknown type, we must have not injected the required runtime
// libraries - hopefully because this is in a test using NonInjectingCompiler.
JSType iterableType =
iterator
.getJSType()
.getTemplateTypeMap()
.getResolvedTemplateType(registry.getIteratorValueTemplate());
JSType makeIteratorType = makeIteratorName.getJSType();
// e.g. replace
// function(Iterator): Array
// with
// function(Iterator): Array
makeIteratorName.setJSType(replaceTemplate(makeIteratorType, ImmutableList.of(iterableType)));
}
return createCall(makeIteratorName, iterator);
}
/**
* Given an iterable like {@code rhs} in
*
* {@code
* for await (lhs of rhs) { block(); }
* }
*
* returns a call node for the {@code rhs} wrapped in a {@code $jscomp.makeAsyncIterator} call.
*
*
{@code
* $jscomp.makeAsyncIterator(rhs)
* }
*/
Node createJSCompMakeAsyncIteratorCall(Node iterable, Scope scope) {
Node makeIteratorAsyncName = createQName(scope, "$jscomp", "makeAsyncIterator");
// Since createCall (currently) doesn't handle templated functions, fill in the template types
// of makeIteratorName manually.
if (isAddingTypes() && !makeIteratorAsyncName.getJSType().isUnknownType()) {
// if makeIteratorName has the unknown type, we must have not injected the required runtime
// libraries - hopefully because this is in a test using NonInjectingCompiler.
// e.g get `number` from `AsyncIterable`
JSType asyncIterableType =
JsIterables.maybeBoxIterableOrAsyncIterable(iterable.getJSType(), registry)
.orElse(unknownType);
JSType makeAsyncIteratorType = makeIteratorAsyncName.getJSType();
// e.g. replace
// function(AsyncIterable): AsyncIterator
// with
// function(AsyncIterable): AsyncIterator
makeIteratorAsyncName.setJSType(
replaceTemplate(makeAsyncIteratorType, ImmutableList.of(asyncIterableType)));
}
return createCall(makeIteratorAsyncName, iterable);
}
private JSType replaceTemplate(JSType templatedType, ImmutableList templateTypes) {
TemplateTypeMap typeMap =
registry
.getEmptyTemplateTypeMap()
.copyWithExtension(templatedType.getTemplateTypeMap().getTemplateKeys(), templateTypes);
TemplateTypeReplacer replacer = TemplateTypeReplacer.forPartialReplacement(registry, typeMap);
return templatedType.visit(replacer);
}
/**
* Creates a reference to $jscomp.AsyncGeneratorWrapper with the template filled in to match the
* original function.
*
* @param originalFunctionType the type of the async generator function that needs transpilation
*/
Node createAsyncGeneratorWrapperReference(JSType originalFunctionType, Scope scope) {
Node ctor = createQName(scope, "$jscomp", "AsyncGeneratorWrapper");
if (isAddingTypes() && !ctor.getJSType().isUnknownType()) {
// if ctor has the unknown type, we must have not injected the required runtime
// libraries - hopefully because this is in a test using NonInjectingCompiler.
// e.g get `number` from `AsyncIterable`
JSType yieldedType =
originalFunctionType
.toMaybeFunctionType()
.getReturnType()
.getTemplateTypeMap()
.getResolvedTemplateType(registry.getAsyncIterableTemplate());
// e.g. replace
// AsyncGeneratorWrapper
// with
// AsyncGeneratorWrapper
ctor.setJSType(replaceTemplate(ctor.getJSType(), ImmutableList.of(yieldedType)));
}
return ctor;
}
/**
* Creates an empty generator function with the correct return type to be an argument to
* $jscomp.AsyncGeneratorWrapper.
*
* @param asyncGeneratorWrapperType the specific type of the $jscomp.AsyncGeneratorWrapper with
* its template filled in. Should be the type on the node returned from
* createAsyncGeneratorWrapperReference.
*/
Node createEmptyAsyncGeneratorWrapperArgument(JSType asyncGeneratorWrapperType) {
JSType generatorType = null;
if (isAddingTypes()) {
if (asyncGeneratorWrapperType.isUnknownType()) {
// Not injecting libraries?
generatorType =
registry.createFunctionType(
replaceTemplate(
getNativeType(JSTypeNative.GENERATOR_TYPE), ImmutableList.of(unknownType)));
} else {
// Generator<$jscomp.AsyncGeneratorWrapper$ActionRecord>
JSType innerFunctionReturnType =
Iterables.getOnlyElement(
asyncGeneratorWrapperType.toMaybeFunctionType().getParameters())
.getJSType();
generatorType = registry.createFunctionType(innerFunctionReturnType);
}
}
return createEmptyGeneratorFunction(generatorType);
}
Node createJscompAsyncExecutePromiseGeneratorFunctionCall(Scope scope, Node generatorFunction) {
Node jscompDotAsyncExecutePromiseGeneratorFunction =
createQName(scope, "$jscomp", "asyncExecutePromiseGeneratorFunction");
// TODO(bradfordcsmith): Maybe update the type to be more specific
// Currently this method expects `function(): !Generator` and returns `Promise`.
// Since we propagate type information only if type checking has already run,
// these unknowns probably don't matter, but we should be able to be more specific with the
// return type at least.
return createCall(jscompDotAsyncExecutePromiseGeneratorFunction, generatorFunction);
}
private JSType getNativeType(JSTypeNative nativeType) {
checkNotNull(registry, "registry is null");
return checkNotNull(
registry.getNativeType(nativeType), "native type not found: %s", nativeType);
}
/**
* Look up the correct type for the given name in the given scope.
*
* Returns the unknown type if no type can be found
*/
private JSType getVarNameType(Scope scope, String name) {
Var var = scope.getVar(name);
JSType type = null;
if (var != null) {
Node nameDefinitionNode = var.getNode();
if (nameDefinitionNode != null) {
type = nameDefinitionNode.getJSType();
}
}
if (type == null) {
// TODO(bradfordcsmith): Consider throwing an error if the type cannot be found.
type = unknownType;
}
return type;
}
private JSType getJsTypeForProperty(Node receiver, String propertyName) {
// NOTE: we use both findPropertyType and getPropertyType because they are subtly
// different: findPropertyType works on JSType, autoboxing scalars and joining unions,
// but it returns null if the type is not found and does not handle dynamic types of
// Function.prototype.call and .apply; whereas getPropertyType does not autobox nor
// iterate over unions, but it does synthesize the function properties correctly, and
// it returns unknown instead of null if the property is missing.
JSType getpropType = null;
JSType receiverJSType = receiver.getJSType();
if (receiverJSType != null) {
getpropType = receiverJSType.findPropertyType(propertyName);
if (getpropType == null) {
ObjectType receiverObjectType = ObjectType.cast(receiverJSType.autobox());
getpropType =
receiverObjectType == null
? unknownType
: receiverObjectType.getPropertyType(propertyName);
}
}
if (getpropType == null) {
getpropType = unknownType;
}
// TODO(bradfordcsmith): Special case $jscomp.global until we annotate its type correctly.
if (getpropType.isUnknownType()
&& propertyName.equals("global")
&& receiver.matchesName("$jscomp")) {
getpropType = getNativeType(JSTypeNative.GLOBAL_THIS);
}
return getpropType;
}
}