Repository: eslint/eslint
Pull request #10685 (2018-07-26 23:48:16)

Chore: rm unnecessary plugin in eslint-config-eslint
it has been added in `node/recommended`(previous included just since it has not been released):
https://github.com/mysticatea/eslint-plugin-node/blob/master/lib/configs/recommended.json#L13




Patch:
diff --git a/conf/eslint-recommended.js b/conf/eslint-recommended.js
index 28d7aa4f9..8b74d8ba9 100644
--- a/conf/eslint-recommended.js
+++ b/conf/eslint-recommended.js
@@ -83,6 +83,7 @@ module.exports = {
         "newline-per-chained-call": "off",
         "no-alert": "off",
         "no-array-constructor": "off",
+        "no-async-promise-executor": "off",
         "no-await-in-loop": "off",
         "no-bitwise": "off",
         "no-buffer-constructor": "off",
@@ -240,6 +241,7 @@ module.exports = {
         "quote-props": "off",
         quotes: "off",
         radix: "off",
+        "require-atomic-updates": "off",
         "require-await": "off",
         "require-jsdoc": "off",
         "require-yield": "error",
diff --git a/docs/rules/no-async-promise-executor.md b/docs/rules/no-async-promise-executor.md
new file mode 100644
index 000000000..82fed5369
--- /dev/null
+++ b/docs/rules/no-async-promise-executor.md
@@ -0,0 +1,62 @@
+# disallow using an async function as a Promise executor (no-async-promise-executor)
+
+The `new Promise` constructor accepts an *executor* function as an argument, which has `resolve` and `reject` parameters that can be used to control the state of the created Promise. For example:
+
+```js
+const result = new Promise(function executor(resolve, reject) {
+  readFile('foo.txt', function(err, result) {
+    if (err) {
+      reject(err);
+    } else {
+      resolve(result);
+    }
+  });
+});
+```
+
+The executor function can also be an `async function`. However, this is usually a mistake, for a few reasons:
+
+* If an async executor function throws an error, the error will be lost and won't cause the newly-constructed `Promise` to reject. This could make it difficult to debug and handle some errors.
+* If a Promise executor function is using `await`, this is usually a sign that it is not actually necessary to use the `new Promise` constructor, or the scope of the `new Promise` constructor can be reduced.
+
+## Rule Details
+
+This rule aims to disallow async Promise executor functions.
+
+Examples of **incorrect** code for this rule:
+
+```js
+const result = new Promise(async (resolve, reject) => {
+  readFile('foo.txt', function(err, result) {
+    if (err) {
+      reject(err);
+    } else {
+      resolve(result);
+    }
+  });
+});
+
+const result = new Promise(async (resolve, reject) => {
+  resolve(await foo);
+});
+```
+
+Examples of **correct** code for this rule:
+
+```js
+const result = new Promise((resolve, reject) => {
+  readFile('foo.txt', function(err, result) {
+    if (err) {
+      reject(err);
+    } else {
+      resolve(result);
+    }
+  });
+});
+
+const result = Promise.resolve(foo);
+```
+
+## When Not To Use It
+
+If your codebase doesn't support async function syntax, there's no need to enable this rule.
diff --git a/docs/rules/require-atomic-updates.md b/docs/rules/require-atomic-updates.md
new file mode 100644
index 000000000..593ccdd60
--- /dev/null
+++ b/docs/rules/require-atomic-updates.md
@@ -0,0 +1,97 @@
+# Disallow assignments that can lead to race conditions due to usage of `await` or `yield` (require-atomic-updates)
+
+When writing asynchronous code, it is possible to create subtle race condition bugs. Consider the following example:
+
+```js
+let totalLength = 0;
+
+async function addLengthOfSinglePage(pageNum) {
+  totalLength += await getPageLength(pageNum);
+}
+
+Promise.all([addLengthOfSinglePage(1), addLengthOfSinglePage(2)]).then(() => {
+  console.log('The combined length of both pages is', totalLength);
+});
+```
+
+This code looks like it will sum the results of calling `getPageLength(1)` and `getPageLength(2)`, but in reality the final value of `totalLength` will only be the length of one of the two pages. The bug is in the statement `totalLength += await getPageLength(pageNum);`. This statement first reads an initial value of `totalLength`, then calls `getPageLength(pageNum)` and waits for that Promise to fulfill. Finally, it sets the value of `totalLength` to the sum of `await getPageLength(pageNum)` and the *initial* value of `totalLength`. If the `totalLength` variable is updated in a separate function call during the time that the `getPageLength(pageNum)` Promise is pending, that update will be lost because the new value is overwritten without being read.
+
+One way to fix this issue would be to ensure that `totalLength` is read at the same time as it's updated, like this:
+
+```js
+async function addLengthOfSinglePage(pageNum) {
+  const lengthOfThisPage = await getPageLength(pageNum);
+
+  totalLength += lengthOfThisPage;
+}
+```
+
+Another solution would be to avoid using a mutable variable reference at all:
+
+```js
+Promise.all([getPageLength(1), getPageLength(2)]).then(pageLengths => {
+  const totalLength = pageLengths.reduce((accumulator, length) => accumulator + length, 0);
+
+  console.log('The combined length of both pages is', totalLength);
+});
+```
+
+## Rule Details
+
+This rule aims to report assignments to variables or properties where all of the following are true:
+
+* A variable or property is reassigned to a new value which is based on its old value.
+* A `yield` or `await` expression interrupts the assignment after the old value is read, and before the new value is set.
+* The rule cannot easily verify that the assignment is safe (e.g. if an assigned variable is local and would not be readable from anywhere else while the function is paused).
+
+Examples of **incorrect** code for this rule:
+
+```js
+/* eslint require-atomic-updates: error */
+
+let result;
+async function foo() {
+  result += await somethingElse;
+
+  result = result + await somethingElse;
+
+  result = result + doSomething(await somethingElse);
+}
+
+function* bar() {
+  result += yield;
+
+  result = result + (yield somethingElse);
+
+  result = result + doSomething(yield somethingElse);
+}
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/* eslint require-atomic-updates: error */
+
+let result;
+async function foo() {
+  result = await somethingElse + result;
+
+  let tmp = await somethingElse;
+  result += tmp;
+
+  let localVariable = 0;
+  localVariable += await somethingElse;
+}
+
+function* bar() {
+  result += yield;
+
+  result = (yield somethingElse) + result;
+
+  result = doSomething(yield somethingElse, result);
+}
+```
+
+## When Not To Use It
+
+If you don't use async or generator functions, you don't need to enable this rule.
diff --git a/lib/rules/indent.js b/lib/rules/indent.js
index a08b4d7e5..d3793bbbb 100644
--- a/lib/rules/indent.js
+++ b/lib/rules/indent.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview This option sets a specific tab width for your code
+ * @fileoverview This rule sets a specific indentation style and width for your code
  *
  * @author Teddy Katz
  * @author Vitaly Puzrin
@@ -855,7 +855,11 @@ module.exports = {
                         previousElement &&
                         previousElementLastToken.loc.end.line - countTrailingLinebreaks(previousElementLastToken.value) > startToken.loc.end.line
                     ) {
-                        offsets.setDesiredOffsets(element.range, firstTokenOfPreviousElement, 0);
+                        offsets.setDesiredOffsets(
+                            [previousElement.range[1], element.range[1]],
+                            firstTokenOfPreviousElement,
+                            0
+                        );
                     }
                 }
             });
@@ -997,6 +1001,31 @@ module.exports = {
             return !node || node.loc.start.line === token.loc.start.line;
         }
 
+        /**
+         * Check whether there are any blank (whitespace-only) lines between
+         * two tokens on separate lines.
+         * @param {Token} firstToken The first token.
+         * @param {Token} secondToken The second token.
+         * @returns {boolean} `true` if the tokens are on separate lines and
+         *   there exists a blank line between them, `false` otherwise.
+         */
+        function hasBlankLinesBetween(firstToken, secondToken) {
+            const firstTokenLine = firstToken.loc.end.line;
+            const secondTokenLine = secondToken.loc.start.line;
+
+            if (firstTokenLine === secondTokenLine || firstTokenLine === secondTokenLine - 1) {
+                return false;
+            }
+
+            for (let line = firstTokenLine + 1; line < secondTokenLine; ++line) {
+                if (!tokenInfo.firstTokensByLineNumber.has(line)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
         const ignoredNodeFirstTokens = new Set();
 
         const baseOffsetListeners = {
@@ -1536,10 +1565,13 @@ module.exports = {
                             const tokenBefore = precedingTokens.get(firstTokenOfLine);
                             const tokenAfter = tokenBefore ? sourceCode.getTokenAfter(tokenBefore) : sourceCode.ast.tokens[0];
 
+                            const mayAlignWithBefore = tokenBefore && !hasBlankLinesBetween(tokenBefore, firstTokenOfLine);
+                            const mayAlignWithAfter = tokenAfter && !hasBlankLinesBetween(firstTokenOfLine, tokenAfter);
+
                             // If a comment matches the expected indentation of the token immediately before or after, don't report it.
                             if (
-                                tokenBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
-                                tokenAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
+                                mayAlignWithBefore && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenBefore)) ||
+                                mayAlignWithAfter && validateTokenIndent(firstTokenOfLine, offsets.getDesiredIndent(tokenAfter))
                             ) {
                                 return;
                             }
diff --git a/lib/rules/max-len.js b/lib/rules/max-len.js
index 273eb8490..88a388309 100644
--- a/lib/rules/max-len.js
+++ b/lib/rules/max-len.js
@@ -266,7 +266,7 @@ module.exports = {
                 // list of comments to ignore
                 comments = ignoreComments || maxCommentLength || ignoreTrailingComments ? sourceCode.getAllComments() : [];
 
-                // we iterate over comments in parallel with the lines
+            // we iterate over comments in parallel with the lines
             let commentsIndex = 0;
 
             const strings = getAllStrings();
diff --git a/lib/rules/no-async-promise-executor.js b/lib/rules/no-async-promise-executor.js
new file mode 100644
index 000000000..55d489eb6
--- /dev/null
+++ b/lib/rules/no-async-promise-executor.js
@@ -0,0 +1,33 @@
+/**
+ * @fileoverview disallow using an async function as a Promise executor
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow using an async function as a Promise executor",
+            category: "Possible Errors",
+            recommended: false,
+            url: "https://eslint.org/docs/rules/no-async-promise-executor"
+        },
+        fixable: null,
+        schema: []
+    },
+
+    create(context) {
+        return {
+            "NewExpression[callee.name='Promise'][arguments.0.async=true]"(node) {
+                context.report({
+                    node: context.getSourceCode().getFirstToken(node.arguments[0], token => token.value === "async"),
+                    message: "Promise executor functions should not be async."
+                });
+            }
+        };
+    }
+};
diff --git a/lib/rules/require-atomic-updates.js b/lib/rules/require-atomic-updates.js
new file mode 100644
index 000000000..2ee5ad21e
--- /dev/null
+++ b/lib/rules/require-atomic-updates.js
@@ -0,0 +1,239 @@
+/**
+ * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
+ * @author Teddy Katz
+ */
+"use strict";
+
+const astUtils = require("../ast-utils");
+
+//------------------------------------------------------------------------------
+// Rule Definition
+//------------------------------------------------------------------------------
+
+module.exports = {
+    meta: {
+        docs: {
+            description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
+            category: "Possible Errors",
+            recommended: false,
+            url: "https://eslint.org/docs/rules/require-atomic-updates"
+        },
+        fixable: null,
+        schema: [],
+        messages: {
+            nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
+        }
+    },
+
+    create(context) {
+        const sourceCode = context.getSourceCode();
+        const identifierToSurroundingFunctionMap = new WeakMap();
+        const expressionsByCodePathSegment = new Map();
+
+        //----------------------------------------------------------------------
+        // Helpers
+        //----------------------------------------------------------------------
+
+        const resolvedVariableCache = new WeakMap();
+
+        /**
+         * Gets the variable scope around this variable reference
+         * @param {ASTNode} identifier An `Identifier` AST node
+         * @returns {Scope|null} An escope Scope
+         */
+        function getScope(identifier) {
+            for (let currentNode = identifier; currentNode; currentNode = currentNode.parent) {
+                const scope = sourceCode.scopeManager.acquire(currentNode, true);
+
+                if (scope) {
+                    return scope;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Resolves a given identifier to a given scope
+         * @param {ASTNode} identifier An `Identifier` AST node
+         * @param {Scope} scope An escope Scope
+         * @returns {Variable|null} An escope Variable corresponding to the given identifier
+         */
+        function resolveVariableInScope(identifier, scope) {
+            return scope.variables.find(variable => variable.name === identifier.name) ||
+                (scope.upper ? resolveVariableInScope(identifier, scope.upper) : null);
+        }
+
+        /**
+         * Resolves an identifier to a variable
+         * @param {ASTNode} identifier An identifier node
+         * @returns {Variable|null} The escope Variable that uses this identifier
+         */
+        function resolveVariable(identifier) {
+            if (!resolvedVariableCache.has(identifier)) {
+                const surroundingScope = getScope(identifier);
+
+                if (surroundingScope) {
+                    resolvedVariableCache.set(identifier, resolveVariableInScope(identifier, surroundingScope));
+                } else {
+                    resolvedVariableCache.set(identifier, null);
+                }
+            }
+
+            return resolvedVariableCache.get(identifier);
+        }
+
+        /**
+         * Checks if an expression is a variable that can only be observed within the given function.
+         * @param {ASTNode} expression The expression to check
+         * @param {ASTNode} surroundingFunction The function node
+         * @returns {boolean} `true` if the expression is a variable which is local to the given function, and is never
+         * referenced in a closure.
+         */
+        function isLocalVariableWithoutEscape(expression, surroundingFunction) {
+            if (expression.type !== "Identifier") {
+                return false;
+            }
+
+            const variable = resolveVariable(expression);
+
+            if (!variable) {
+                return false;
+            }
+
+            return variable.references.every(reference => identifierToSurroundingFunctionMap.get(reference.identifier) === surroundingFunction) &&
+                variable.defs.every(def => identifierToSurroundingFunctionMap.get(def.name) === surroundingFunction);
+        }
+
+        /**
+         * Reports an AssignmentExpression node that has a non-atomic update
+         * @param {ASTNode} assignmentExpression The assignment that is potentially unsafe
+         * @returns {void}
+         */
+        function reportAssignment(assignmentExpression) {
+            context.report({
+                node: assignmentExpression,
+                messageId: "nonAtomicUpdate",
+                data: {
+                    value: sourceCode.getText(assignmentExpression.left)
+                }
+            });
+        }
+
+        /**
+         * If the control flow graph of a function enters an assignment expression, then does the
+         * both of the following steps in order (possibly with other steps in between) before exiting the
+         * assignment expression, then the assignment might be using an outdated value.
+         * 1. Enters a read of the variable or property assigned in the expression (not necessary if operator assignment is used)
+         * 2. Exits an `await` or `yield` expression
+         *
+         * This function checks for the outdated values and reports them.
+         * @param {CodePathSegment} codePathSegment The current code path segment to traverse
+         * @param {ASTNode} surroundingFunction The function node containing the code path segment
+         * @returns {void}
+         */
+        function findOutdatedReads(
+            codePathSegment,
+            surroundingFunction,
+            {
+                seenSegments = new Set(),
+                openAssignmentsWithoutReads = new Set(),
+                openAssignmentsWithReads = new Set()
+            } = {}
+        ) {
+            if (seenSegments.has(codePathSegment)) {
+
+                // An AssignmentExpression can't contain loops, so it's not necessary to reenter them with new state.
+                return;
+            }
+
+            expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => {
+                if (node.type === "AssignmentExpression") {
+                    if (entering) {
+                        (node.operator === "=" ? openAssignmentsWithoutReads : openAssignmentsWithReads).add(node);
+                    } else {
+                        openAssignmentsWithoutReads.delete(node);
+                        openAssignmentsWithReads.delete(node);
+                    }
+                } else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) {
+                    [...openAssignmentsWithReads]
+                        .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction))
+                        .forEach(reportAssignment);
+
+                    openAssignmentsWithReads.clear();
+                } else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) {
+                    [...openAssignmentsWithoutReads]
+                        .filter(assignment => (
+                            assignment.left !== node &&
+                            assignment.left.type === node.type &&
+                            astUtils.equalTokens(assignment.left, node, sourceCode)
+                        ))
+                        .forEach(assignment => {
+                            openAssignmentsWithoutReads.delete(assignment);
+                            openAssignmentsWithReads.add(assignment);
+                        });
+                }
+            });
+
+            codePathSegment.nextSegments.forEach(nextSegment => {
+                findOutdatedReads(
+                    nextSegment,
+                    surroundingFunction,
+                    {
+                        seenSegments: new Set(seenSegments).add(codePathSegment),
+                        openAssignmentsWithoutReads: new Set(openAssignmentsWithoutReads),
+                        openAssignmentsWithReads: new Set(openAssignmentsWithReads)
+                    }
+                );
+            });
+        }
+
+        //----------------------------------------------------------------------
+        // Public
+        //----------------------------------------------------------------------
+
+        const currentCodePathSegmentStack = [];
+        let currentCodePathSegment = null;
+        const functionStack = [];
+
+        return {
+            onCodePathStart() {
+                currentCodePathSegmentStack.push(currentCodePathSegment);
+            },
+
+            onCodePathEnd(codePath, node) {
+                currentCodePathSegment = currentCodePathSegmentStack.pop();
+
+                if (astUtils.isFunction(node) && (node.async || node.generator)) {
+                    findOutdatedReads(codePath.initialSegment, node);
+                }
+            },
+
+            onCodePathSegmentStart(segment) {
+                currentCodePathSegment = segment;
+                expressionsByCodePathSegment.set(segment, []);
+            },
+
+            "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression"(node) {
+                expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: true, node });
+            },
+
+            "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression:exit"(node) {
+                expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: false, node });
+            },
+
+            ":function"(node) {
+                functionStack.push(node);
+            },
+
+            ":function:exit"() {
+                functionStack.pop();
+            },
+
+            Identifier(node) {
+                if (functionStack.length) {
+                    identifierToSurroundingFunctionMap.set(node, functionStack[functionStack.length - 1]);
+                }
+            }
+        };
+    }
+};
diff --git a/packages/eslint-config-eslint/default.yml b/packages/eslint-config-eslint/default.yml
index da4faf6e5..127e7d7d7 100644
--- a/packages/eslint-config-eslint/default.yml
+++ b/packages/eslint-config-eslint/default.yml
@@ -1,10 +1,6 @@
 extends:
     - "eslint:recommended"
     - "plugin:node/recommended"
-
-plugins:
-    - "node"
-
 rules:
     array-bracket-spacing: "error"
     array-callback-return: "error"
@@ -54,6 +50,7 @@ rules:
     new-parens: "error"
     no-alert: "error"
     no-array-constructor: "error"
+    no-async-promise-executor: "error"
     no-buffer-constructor: "error"
     no-caller: "error"
     no-catch-shadow: "error"
@@ -160,6 +157,7 @@ rules:
     quotes: ["error", "double"]
     quote-props: ["error", "as-needed"]
     radix: "error"
+    require-atomic-updates: "error"
     require-jsdoc: "error"
     rest-spread-spacing: "error"
     semi: "error"


Test patch:
diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js
index ab8ba4c18..b25e25949 100644
--- a/tests/lib/rules/indent.js
+++ b/tests/lib/rules/indent.js
@@ -64,7 +64,7 @@ function unIndent(strings) {
     const templateValue = strings[0];
     const lines = templateValue.replace(/^\n/, "").replace(/\n\s*$/, "").split("\n");
     const lineIndents = lines.filter(line => line.trim()).map(line => line.match(/ */)[0].length);
-    const minLineIndent = Math.min.apply(null, lineIndents);
+    const minLineIndent = Math.min(...lineIndents);
 
     return lines.map(line => line.slice(minLineIndent)).join("\n");
 }
@@ -4843,6 +4843,56 @@ ruleTester.run("indent", rule, {
                         2
                 }
             }
+        `,
+
+        //----------------------------------------------------------------------
+        // Comment alignment tests
+        //----------------------------------------------------------------------
+        unIndent`
+            if (foo) {
+            // Comment can align with code immediately above even if "incorrect" alignment
+                doSomething();
+            }
+        `,
+        unIndent`
+            if (foo) {
+                doSomething();
+            // Comment can align with code immediately below even if "incorrect" alignment
+            }
+        `,
+        unIndent`
+            if (foo) {
+                // Comment can be in correct alignment even if not aligned with code above/below
+            }
+        `,
+        unIndent`
+            if (foo) {
+
+                // Comment can be in correct alignment even if gaps between (and not aligned with) code above/below
+
+            }
+        `,
+        unIndent`
+            [{
+                foo
+            },
+
+            // Comment between nodes
+
+            {
+                bar
+            }];
+        `,
+        unIndent`
+            [{
+                foo
+            },
+
+            // Comment between nodes
+
+            { // comment
+                bar
+            }];
         `
     ],
 
@@ -9383,6 +9433,71 @@ ruleTester.run("indent", rule, {
                 }
             `,
             errors: expectedErrors([4, 12, 8, "Numeric"])
+        },
+
+        //----------------------------------------------------------------------
+        // Comment alignment tests
+        //----------------------------------------------------------------------
+        {
+            code: unIndent`
+                if (foo) {
+
+                // Comment cannot align with code immediately above if there is a whitespace gap
+                    doSomething();
+                }
+            `,
+            output: unIndent`
+                if (foo) {
+
+                    // Comment cannot align with code immediately above if there is a whitespace gap
+                    doSomething();
+                }
+            `,
+            errors: expectedErrors([3, 4, 0, "Line"])
+        },
+        {
+            code: unIndent`
+                if (foo) {
+                    foo(
+                        bar);
+                // Comment cannot align with code immediately below if there is a whitespace gap
+
+                }
+            `,
+            output: unIndent`
+                if (foo) {
+                    foo(
+                        bar);
+                    // Comment cannot align with code immediately below if there is a whitespace gap
+
+                }
+            `,
+            errors: expectedErrors([4, 4, 0, "Line"])
+        },
+        {
+            code: unIndent`
+                [{
+                    foo
+                },
+
+                    // Comment between nodes
+
+                {
+                    bar
+                }];
+            `,
+            output: unIndent`
+                [{
+                    foo
+                },
+
+                // Comment between nodes
+
+                {
+                    bar
+                }];
+            `,
+            errors: expectedErrors([5, 0, 4, "Line"])
         }
     ]
 });
diff --git a/tests/lib/rules/no-async-promise-executor.js b/tests/lib/rules/no-async-promise-executor.js
new file mode 100644
index 000000000..d5bcd0ae9
--- /dev/null
+++ b/tests/lib/rules/no-async-promise-executor.js
@@ -0,0 +1,61 @@
+/**
+ * @fileoverview disallow using an async function as a Promise executor
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/no-async-promise-executor");
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 8 } });
+
+ruleTester.run("no-async-promise-executor", rule, {
+
+    valid: [
+        "new Promise((resolve, reject) => {})",
+        "new Promise((resolve, reject) => {}, async function unrelated() {})",
+        "new Foo(async (resolve, reject) => {})"
+    ],
+
+    invalid: [
+        {
+            code: "new Promise(async function foo(resolve, reject) {})",
+            errors: [{
+                message: "Promise executor functions should not be async.",
+                line: 1,
+                column: 13,
+                endLine: 1,
+                endColumn: 18
+            }]
+        },
+        {
+            code: "new Promise(async (resolve, reject) => {})",
+            errors: [{
+                message: "Promise executor functions should not be async.",
+                line: 1,
+                column: 13,
+                endLine: 1,
+                endColumn: 18
+            }]
+        },
+        {
+            code: "new Promise(((((async () => {})))))",
+            errors: [{
+                message: "Promise executor functions should not be async.",
+                line: 1,
+                column: 17,
+                endLine: 1,
+                endColumn: 22
+            }]
+        }
+    ]
+});
diff --git a/tests/lib/rules/require-atomic-updates.js b/tests/lib/rules/require-atomic-updates.js
new file mode 100644
index 000000000..eabd52149
--- /dev/null
+++ b/tests/lib/rules/require-atomic-updates.js
@@ -0,0 +1,143 @@
+/**
+ * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
+ * @author Teddy Katz
+ */
+"use strict";
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require("../../../lib/rules/require-atomic-updates");
+const RuleTester = require("../../../lib/testers/rule-tester");
+
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2018 } });
+
+const VARIABLE_ERROR = {
+    messageId: "nonAtomicUpdate",
+    data: { value: "foo" },
+    type: "AssignmentExpression"
+};
+
+const STATIC_PROPERTY_ERROR = {
+    messageId: "nonAtomicUpdate",
+    data: { value: "foo.bar" },
+    type: "AssignmentExpression"
+};
+
+const COMPUTED_PROPERTY_ERROR = {
+    messageId: "nonAtomicUpdate",
+    data: { value: "foo[bar].baz" },
+    type: "AssignmentExpression"
+};
+
+ruleTester.run("require-atomic-updates", rule, {
+
+    valid: [
+        "let foo; async function x() { foo += bar; }",
+        "let foo; async function x() { foo = foo + bar; }",
+        "let foo; async function x() { foo = await bar + foo; }",
+        "async function x() { let foo; foo += await bar; }",
+        "let foo; async function x() { foo = (await result)(foo); }",
+        "let foo; async function x() { foo = bar(await something, foo) }",
+        "function* x() { let foo; foo += yield bar; }",
+        "const foo = {}; async function x() { foo.bar = await baz; }",
+        "const foo = []; async function x() { foo[x] += 1;  }",
+        "let foo; function* x() { foo = bar + foo; }",
+        "async function x() { let foo; bar(() => baz += 1); foo += await amount; }",
+        "let foo; async function x() { foo = condition ? foo : await bar; }",
+        "async function x() { let foo; bar(() => { let foo; blah(foo); }); foo += await result; }"
+    ],
+
+    invalid: [
+        {
+            code: "let foo; async function x() { foo += await amount; }",
+            errors: [{ messageId: "nonAtomicUpdate", data: { value: "foo" } }]
+        },
+        {
+            code: "let foo; async function x() { while (condition) { foo += await amount; } }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = foo + await amount; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = foo + (bar ? baz : await amount); }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = foo + (bar ? await amount : baz); }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = condition ? foo + await amount : somethingElse; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = (condition ? foo : await bar) + await bar; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo += bar + await amount; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "async function x() { let foo; bar(() => foo); foo += await amount; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; function* x() { foo += yield baz }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = bar(foo, await something) }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "const foo = {}; async function x() { foo.bar += await baz }",
+            errors: [STATIC_PROPERTY_ERROR]
+        },
+        {
+            code: "const foo = []; async function x() { foo[bar].baz += await result;  }",
+            errors: [COMPUTED_PROPERTY_ERROR]
+        },
+        {
+            code: "let foo; async function* x() { foo = (yield foo) + await bar; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = foo + await result(foo); }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = await result(foo, await somethingElse); }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "function* x() { let foo; yield async function y() { foo += await bar; } }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function* x() { foo = await foo + (yield bar); }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo; async function x() { foo = bar + await foo; }",
+            errors: [VARIABLE_ERROR]
+        },
+        {
+            code: "let foo = {}; async function x() { foo[bar].baz = await (foo.bar += await foo[bar].baz) }",
+            errors: [COMPUTED_PROPERTY_ERROR, STATIC_PROPERTY_ERROR]
+        },
+        {
+            code: "async function x() { foo += await bar; }",
+            errors: [VARIABLE_ERROR]
+        }
+    ]
+});