Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add shim for ES Promise #33863

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"allowDeclarations": true
}],
"no-double-space": "error",
"async-type": "error",
"boolean-trivia": "error",
"no-in-operator": "error",
"simple-indent": "error",
Expand Down
40 changes: 40 additions & 0 deletions scripts/eslint/rules/async-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { AST_NODE_TYPES, TSESTree } from "@typescript-eslint/experimental-utils";
import { createRule } from "./utils";

export = createRule({
name: "async-type",
meta: {
docs: {
description: ``,
category: "Possible Errors",
recommended: "error",
},
messages: {
asyncTypeError: `Async functions must have an explicit return type`,
},
schema: [],
type: "problem",
},
defaultOptions: [],

create(context) {
const checkAsyncFunction = (node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression | TSESTree.ArrowFunctionExpression) => {
if (node.async && !node.returnType) {
context.report({ messageId: "asyncTypeError", node });
}
};

const checkAsyncMethod = (node: TSESTree.MethodDefinition) => {
if (node.value.type === AST_NODE_TYPES.FunctionExpression) {
checkAsyncFunction(node.value);
}
};

return {
[AST_NODE_TYPES.FunctionDeclaration]: checkAsyncFunction,
[AST_NODE_TYPES.FunctionExpression]: checkAsyncFunction,
[AST_NODE_TYPES.ArrowFunctionExpression]: checkAsyncFunction,
[AST_NODE_TYPES.MethodDefinition]: checkAsyncMethod
};
},
});
55 changes: 55 additions & 0 deletions scripts/eslint/tests/async-type.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { RuleTester } from "./support/RuleTester";
import rule = require("../rules/async-type");

const ruleTester = new RuleTester({
parserOptions: {
warnOnUnsupportedTypeScriptVersion: false,
},
parser: require.resolve("@typescript-eslint/parser"),
});

ruleTester.run("async-type", rule, {
valid: [
{
code: `
async function foo(): Promise<void> {}
`,
},
{
code: `
const fn = async function(): Promise<void> {}
`,
},
{
code: `
const fn = async (): Promise<void> => {}
`,
},
{
code: `
class C {
async method(): Promise<void> {}
}
`,
},
],

invalid: [
{
code: `async function foo() {}`,
errors: [{ messageId: "asyncTypeError" }]
},
{
code: `const fn = async function() {}`,
errors: [{ messageId: "asyncTypeError" }]
},
{
code: `const fn = async () => {}`,
errors: [{ messageId: "asyncTypeError" }]
},
{
code: `class C { async method() {} }`,
errors: [{ messageId: "asyncTypeError" }]
},
]
});
2 changes: 2 additions & 0 deletions scripts/open-cherry-pick-pr.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable async-type */
/// <reference lib="esnext.asynciterable" />
// Must reference esnext.asynciterable lib, since octokit uses AsyncIterable internally
/// <reference types="node" />


import Octokit = require("@octokit/rest");
const {runSequence} = require("./run-sequence");
import fs = require("fs");
Expand Down
2 changes: 2 additions & 0 deletions scripts/open-user-pr.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/* eslint-disable async-type */
/// <reference lib="esnext.asynciterable" />
/// <reference lib="es2015.promise" />

// Must reference esnext.asynciterable lib, since octokit uses AsyncIterable internally
import Octokit = require("@octokit/rest");
import {runSequence} from "./run-sequence";
Expand Down
1 change: 1 addition & 0 deletions scripts/produceLKG.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable async-type */
/// <reference types="node" />

import childProcess = require("child_process");
Expand Down
34 changes: 32 additions & 2 deletions src/compiler/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,23 @@ namespace ts {
push(...values: T[]): void;
}

/* @internal */
export interface PromiseConstructor extends PromiseConstructorLike {
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: unknown) => void) => void): Promise<T>;
prototype: Promise<any>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
reject<T = never>(reason: unknown): Promise<T>;
all<T>(promises: (T | PromiseLike<T>)[]): Promise<T[]>;
race<T>(promises: (T | PromiseLike<T>)[]): Promise<T>;
}

/* @internal */
export interface Promise<T> {
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>;
}

/* @internal */
export type EqualityComparer<T> = (a: T, b: T) => boolean;

Expand All @@ -75,8 +92,9 @@ namespace ts {
/* @internal */
namespace ts {
// Natives
// NOTE: This must be declared in a separate block from the one below so that we don't collide with the exported definition of `Map`.
declare const Map: (new <T>() => Map<T>) | undefined;
// NOTE: These must be declared in a separate block from the one below so that we don't collide with the exported definitions of `Map` and `Promise`.
declare const Map: MapConstructor | undefined;
declare const Promise: PromiseConstructor | undefined;

/**
* Returns the native Map implementation if it is available and compatible (i.e. supports iteration).
Expand All @@ -86,6 +104,10 @@ namespace ts {
// eslint-disable-next-line no-in-operator
return typeof Map !== "undefined" && "entries" in Map.prototype ? Map : undefined;
}

export function tryGetNativePromise(): PromiseConstructor | undefined {
return typeof Promise === "function" ? Promise : undefined;
}
}

/* @internal */
Expand All @@ -100,6 +122,14 @@ namespace ts {
throw new Error("TypeScript requires an environment that provides a compatible native Map implementation.");
})();

export const Promise: PromiseConstructor = tryGetNativePromise() || (() => {
// NOTE: createPromiseShim will be defined for typescriptServices.js but not for tsc.js, so we must test for it.
if (typeof createPromiseShim === "function") {
return createPromiseShim();
}
throw new Error("TypeScript requires an environment that provides a compatible native Promise implementation.");
})();

/** Create a new map. */
export function createMap<T>(): Map<T> {
return new Map<T>();
Expand Down
172 changes: 172 additions & 0 deletions src/shims/promiseShim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/* @internal */
namespace ts {
declare function setTimeout<A extends any[]>(handler: (...args: A) => void, timeout: number, ...args: A): any;

export interface Promise<T> extends globalThis.Promise<T> {
}

export interface PromiseConstructor extends PromiseConstructorLike {
new <T>(executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: unknown) => void) => void): Promise<T>;
resolve<T>(value: T | PromiseLike<T>): Promise<T>;
resolve(): Promise<void>;
reject<T = never>(reason: unknown): Promise<T>;
all<T>(promises: (T | PromiseLike<T>)[]): Promise<T[]>;
race<T>(promises: (T | PromiseLike<T>)[]): Promise<T>;
}

export function createPromiseShim(): PromiseConstructor {
class Promise<T> implements ts.Promise<T> {
private _state: "pending" | "fulfilled" | "rejected" = "pending";
private _result: unknown;
private _reactions: PromiseReaction[] = [];

constructor(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason: any) => void) => void) {
const { resolve, reject } = createResolvingFunctions(this);
try {
executor(resolve, reject);
}
catch (e) {
reject(e);
}
}

then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return new Promise<TResult1 | TResult2>((resolve, reject) => {
const reaction: PromiseReaction = { resolve, reject, onfulfilled, onrejected };
if (this._state === "pending") {
this._reactions.push(reaction);
}
else {
setTimeout(promiseReactionJob, 0, reaction, this._state === "fulfilled" ? "fulfill" : "reject", this._result);
}
});
}

catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult> {
return this.then(/*onfulfilled*/ undefined, onrejected);
}

static resolve(): Promise<void>;
static resolve<T>(value: T | PromiseLike<T>): Promise<T>;
static resolve<T>(value?: T | PromiseLike<T>): Promise<T> {
return value instanceof this && value.constructor === this ? value : new Promise<T>(resolve => resolve(value));
}

static reject<T = never>(reason: any): Promise<T> {
return new Promise<T>((_, reject) => reject(reason));
}

static all<T>(promises: (T | PromiseLike<T>)[]): Promise<T[]> {
return new Promise<T[]>((resolve, reject) => {
let count = promises.length;
const values: T[] = Array<T>(count);
for (let i = 0; i < promises.length; i++) {
let called = false;
this.resolve(promises[i]).then(
value => {
if (!called) {
called = true;
values[i] = value;
count--;
if (count === 0) {
resolve(values);
}
}
},
reject);
}
});
}

static race<T>(promises: (T | PromiseLike<T>)[]): Promise<T> {
return new Promise<T>((resolve, reject) => {
for (const promise of promises) {
this.resolve(promise).then(resolve, reject);
}
});
}
}

interface PromiseReaction {
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
onfulfilled?: ((value: unknown) => unknown) | null;
onrejected?: ((reason: unknown) => unknown) | null;
}

function createResolvingFunctions<T>(promise: Promise<T>) {
let called = false;
return {
resolve: (value: T | Promise<T>) => {
if (!called) {
called = true;
try {
if (promise === value) throw new TypeError();
// eslint-disable-next-line no-null/no-null
const then = typeof value === "object" && value !== null && (<Promise<unknown>>value).then;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const then = typeof value === "object" && value !== null && (<Promise<unknown>>value).then;
const then = ((typeof value === "object" && value !== null) || typeof value === "function") && (<Promise<unknown>>value).then;

if (typeof then !== "function") {
settlePromise(promise, "fulfill", value);
}
else {
setTimeout(resolveThenableJob, 0, promise, value, then);
}
}
catch (e) {
settlePromise(promise, "reject", e);
}
}
},
reject: (reason: any) => {
if (!called) {
called = true;
settlePromise(promise, "reject", reason);
}
}
};
}

function settlePromise(promise: Promise<unknown>, verb: "fulfill" | "reject", value: unknown) {
/* eslint-disable dot-notation */
const reactions = promise["_reactions"];
promise["_result"] = value;
promise["_reactions"] = undefined!;
promise["_state"] = verb === "fulfill" ? "fulfilled" : "rejected";
for (const reaction of reactions) {
setTimeout(promiseReactionJob, 0, reaction, verb, value);
}
/* eslint-enable dot-notation */
}

function resolveThenableJob<T>(promiseToResolve: Promise<T>, thenable: T, thenAction: Promise<T>["then"]) {
const { resolve, reject } = createResolvingFunctions(promiseToResolve);
try {
thenAction.call(thenable, resolve, reject);
}
catch (e) {
reject(e);
}
}

function promiseReactionJob(reaction: PromiseReaction, verb: "fulfill" | "reject", argument: unknown) {
const handler = verb === "fulfill" ? reaction.onfulfilled : reaction.onrejected;
if (handler) {
try {
argument = handler(argument);
verb = "fulfill";
}
catch (e) {
argument = e;
verb = "reject";
}
}

const action = verb === "fulfill" ? reaction.resolve : reaction.reject;
action(argument);
}

return Promise;
}
}
3 changes: 2 additions & 1 deletion src/shims/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"outFile": "../../built/local/shims.js"
},
"files": [
"mapShim.ts"
"mapShim.ts",
"promiseShim.ts"
]
}
1 change: 1 addition & 0 deletions src/testRunner/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"unittests/reuseProgramStructure.ts",
"unittests/semver.ts",
"unittests/createMapShim.ts",
"unittests/createPromiseShim.ts",
"unittests/transform.ts",
"unittests/config/commandLineParsing.ts",
"unittests/config/configurationExtension.ts",
Expand Down
Loading