com.google.javascript.jscomp.Promises Maven / Gradle / Ivy
Show all versions of closure-compiler-linter 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.collect.ImmutableList.toImmutableList;
import com.google.common.collect.ImmutableList;
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.UnionTypeBuilder;
/**
* Models different Javascript Promise-related operations
*
* @author [email protected] (Laura Harker)
*/
final class Promises {
private Promises() {}
/**
* If this object is known to be an IThenable, returns the type it resolves to.
*
* Returns unknown otherwise.
*
*
(This is different from {@code getResolvedType}, which will attempt to model the then type
* of an expression after calling Promise.resolve() on it.
*/
static final JSType getTemplateTypeOfThenable(JSTypeRegistry registry, JSType maybeThenable) {
return maybeThenable
// Without ".restrictByNotNullOrUndefined" we'd get the unknown type for "?IThenable"
.restrictByNotNullOrUndefined()
.getInstantiatedTypeArgument(registry.getNativeType(JSTypeNative.I_THENABLE_TYPE));
}
/**
* Returns the type of `await [expr]`.
*
* This is equivalent to the type of `result` in `Promise.resolve([expr]).then(result => `
*
*
For example:
*
*
{@code !Promise} becomes {@code number}
*
* {@code !IThenable} becomes {@code number}
*
* {@code string} becomes {@code string}
*
*
{@code (!Promise|string)} becomes {@code (number|string)}
*
* {@code ?Promise} becomes {@code (null|number)}
*/
static final JSType getResolvedType(JSTypeRegistry registry, JSType type) {
if (type.isUnknownType()) {
return type;
}
if (type.isUnionType()) {
UnionTypeBuilder unionTypeBuilder = UnionTypeBuilder.create(registry);
for (JSType alternate : type.toMaybeUnionType().getAlternates()) {
unionTypeBuilder.addAlternate(getResolvedType(registry, alternate));
}
return unionTypeBuilder.build();
}
// If we can find the "IThenable" template key (which is true for Promise and IThenable), return
// the resolved value. e.g. for "!Promise" return "string".
TemplateTypeMap templates = type.getTemplateTypeMap();
if (templates.hasTemplateKey(registry.getIThenableTemplate())) {
// Call getResolvedPromiseType again in case someone does something unusual like
// !Promise>
// TODO(lharker): we don't need to handle this case and should report an error for this in a
// type annotation (not here, maybe in TypeCheck). A Promise cannot resolve to another Promise
return getResolvedType(
registry, templates.getResolvedTemplateType(registry.getIThenableTemplate()));
}
// Awaiting anything with a ".then" property (other than IThenable, handled above) should return
// unknown, rather than the type itself.
if (type.isSubtypeOf(registry.getNativeType(JSTypeNative.THENABLE_TYPE))) {
return registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
}
return type;
}
/**
* Wraps the given type in an IThenable.
*
* If the given type is already IThenable it is first unwrapped. For example:
*
*
{@code number} becomes {@code IThenable}
*
* {@code IThenable} becomes {@code IThenable}
*
* {@code Promise} becomes {@code IThenable}
*
* {@code IThenable|string} becomes {@code IThenable}
*
* {@code IThenable|IThenable} becomes {@code IThenable}
*/
static final JSType wrapInIThenable(JSTypeRegistry registry, JSType maybeThenable) {
// Unwrap for simplicity first in the event it is a thenable.
JSType unwrapped = getResolvedType(registry, maybeThenable);
return registry.createTemplatizedType(
registry.getNativeObjectType(JSTypeNative.I_THENABLE_TYPE), unwrapped);
}
/**
* Synthesizes a type representing the legal types of a return expression within async code
* (i.e.`Promise` callbacks, async functions) based on the expected return type of that code.
*
* The return type will generally be a union but may not be in the case of top-like types. If
* the expected return type is a union, any synchronous elements will be dropped, since they can
* never occur. For example:
*
*
* - `!Promise
` => `number|!IThenable`
* - `number` => `?`
*
- `number|!Promise
` => `string|!IThenable`
* - `!IThenable
|!Promise` => `number|string|!IThenable`
* - `!IThenable
` => `number|string|!IThenable`
* - `?` => `?`
*
- `*` => `?`
*
*/
static final JSType createAsyncReturnableType(JSTypeRegistry registry, JSType maybeThenable) {
JSType unknownType = registry.getNativeType(JSTypeNative.UNKNOWN_TYPE);
ObjectType iThenableType = registry.getNativeObjectType(JSTypeNative.I_THENABLE_TYPE);
JSType iThenableOfUnknownType = registry.createTemplatizedType(iThenableType, unknownType);
ImmutableList alternates =
maybeThenable.isUnionType()
? maybeThenable.toMaybeUnionType().getAlternates()
: ImmutableList.of(maybeThenable);
ImmutableList asyncTemplateAlternates =
alternates.stream()
.filter((t) -> t.isSubtypeOf(iThenableOfUnknownType)) // Discard "synchronous" types.
.map((t) -> getTemplateTypeOfThenable(registry, t)) // Unwrap "asynchronous" types.
.collect(toImmutableList());
if (asyncTemplateAlternates.isEmpty()) {
return unknownType;
}
JSType asyncTemplateUnion = registry.createUnionType(asyncTemplateAlternates);
return registry.createUnionType(
asyncTemplateUnion, registry.createTemplatizedType(iThenableType, asyncTemplateUnion));
}
}