Repository: eslint/eslint (js)

Issue:
[Feature request] disallow async promise executor
**Please describe what the rule should do:**

Promise executors must not be (technically, they can if you know what you're doing)  `async` else bad side effects happen, such as throwing (seemingly) sync errors `(A)` no longer rejecting the promise (thus unable to be caught by `(B)`), because the async function is executed in different frame:

```js
new Promise( async r => {
    throw new Error("aaa"); // (A)
})
.catch(() => {}) // (B)
```

Why is this different from not catching any potential async errors in a non-`async` executor? It isn't -- but, the above example gives off a feeling like you're throwing a sync error, while it's not, and for people new to async/await this may not be evident.

More info in this [SO answer](https://stackoverflow.com/a/43083793/927631).

Thus, I propose a rule that forbids `async` [*executor* function](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise).

**What category of rule is this? (place an "X" next to just one item)**

[ ] Enforces code style
[x] Warns about a potential error
[ ] Suggests an alternate way of doing something
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule will warn about:**

<!-- Put your code examples here -->
```js
new Promise( async function (resolve, reject) {
    // any code
});
```

```js
new Promise( async (resolve, reject) => {
    // any code
});
```

```js
const executor = async function (resolve, reject) {
    // any code
};
new Promise( executor );
```

**Why should this rule be included in ESLint (instead of a plugin)?**

It seems like to become common-enough error when async/await becomes more mainstream to include it in core.

Pull request description:
New: add no-async-promise-executor rule (fixes #10217)
<!--
    ESLint adheres to the [JS Foundation Code of Conduct](https://js.foundation/community/code-of-conduct).
-->

**What is the purpose of this pull request? (put an "X" next to item)**

[x] New rule (https://github.com/eslint/eslint/issues/10217)

<!--
    If the item you've checked above has a template, please paste the template questions below and answer them. (If this pull request is addressing an issue, you can just paste a link to the issue here instead.)
-->

<!--
    Please ensure your pull request is ready:

    - Read the pull request guide (https://eslint.org/docs/developer-guide/contributing/pull-requests)
    - Include tests for this change
    - Update documentation for this change (if appropriate)
-->

<!--
    The following is required for all pull requests:
-->

**What changes did you make? (Give an overview)**

This implements the `no-async-promise-executor` rule, as described in https://github.com/eslint/eslint/issues/10217.

**Is there anything you'd like reviewers to focus on?**

Nothing in particular

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/no-prototype-builtins.md b/docs/rules/no-prototype-builtins.md
index 1694356a3..e2dd4f5b9 100644
--- a/docs/rules/no-prototype-builtins.md
+++ b/docs/rules/no-prototype-builtins.md
@@ -1,6 +1,6 @@
 # Disallow use of Object.prototypes builtins directly (no-prototype-builtins)
 
-In ECMAScript 5.1, `Object.create` was added, which enables the creation of objects with a specified `[[Prototype]]`. `Object.create(null)` is a common pattern used to create objects that will be used as a Map. This can lead to errors when it is assumed that objects will have properties from `Object.prototype`. This rule prevents calling `Object.prototype` methods directly from an object.
+In ECMAScript 5.1, `Object.create` was added, which enables the creation of objects with a specified `[[Prototype]]`. `Object.create(null)` is a common pattern used to create objects that will be used as a Map. This can lead to errors when it is assumed that objects will have properties from `Object.prototype`. This rule prevents calling some `Object.prototype` methods directly from an object.
 
 ## Rule Details
 
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/ignored-paths.js b/lib/ignored-paths.js
index 8134d6d32..4ef15ae75 100644
--- a/lib/ignored-paths.js
+++ b/lib/ignored-paths.js
@@ -79,6 +79,27 @@ function mergeDefaultOptions(options) {
     return Object.assign({}, DEFAULT_OPTIONS, options);
 }
 
+/**
+ * Converts a glob pattern to a new glob pattern relative to a different directory
+ * @param {string} globPattern The glob pattern, relative the the old base directory
+ * @param {string} relativePathToOldBaseDir A relative path from the new base directory to the old one
+ * @returns {string} A glob pattern relative to the new base directory
+ */
+function relativize(globPattern, relativePathToOldBaseDir) {
+    if (relativePathToOldBaseDir === "") {
+        return globPattern;
+    }
+
+    const prefix = globPattern.startsWith("!") ? "!" : "";
+    const globWithoutPrefix = globPattern.replace(/^!/, "");
+
+    if (globWithoutPrefix.startsWith("/")) {
+        return `${prefix}/${relativePathToOldBaseDir}${globWithoutPrefix}`;
+    }
+
+    return globPattern;
+}
+
 //------------------------------------------------------------------------------
 // Public Interface
 //------------------------------------------------------------------------------
@@ -96,41 +117,35 @@ class IgnoredPaths {
 
         this.cache = {};
 
-        /**
-         * add pattern to node-ignore instance
-         * @param {Object} ig, instance of node-ignore
-         * @param {string} pattern, pattern do add to ig
-         * @returns {array} raw ignore rules
-         */
-        function addPattern(ig, pattern) {
-            return ig.addPattern(pattern);
-        }
-
         this.defaultPatterns = [].concat(DEFAULT_IGNORE_DIRS, options.patterns || []);
-        this.baseDir = options.cwd;
+
+        this.ignoreFileDir = options.ignore !== false && options.ignorePath
+            ? path.dirname(path.resolve(options.cwd, options.ignorePath))
+            : options.cwd;
+        this.options = options;
 
         this.ig = {
             custom: ignore(),
             default: ignore()
         };
 
-        /*
-         * Add a way to keep track of ignored files.  This was present in node-ignore
-         * 2.x, but dropped for now as of 3.0.10.
-         */
-        this.ig.custom.ignoreFiles = [];
-        this.ig.default.ignoreFiles = [];
-
+        this.defaultPatterns.forEach(pattern => this.addPatternRelativeToCwd(this.ig.default, pattern));
         if (options.dotfiles !== true) {
 
             /*
              * ignore files beginning with a dot, but not files in a parent or
              * ancestor directory (which in relative format will begin with `../`).
              */
-            addPattern(this.ig.default, [".*", "!../"]);
+            this.addPatternRelativeToCwd(this.ig.default, ".*");
+            this.addPatternRelativeToCwd(this.ig.default, "!../");
         }
 
-        addPattern(this.ig.default, this.defaultPatterns);
+        /*
+         * Add a way to keep track of ignored files.  This was present in node-ignore
+         * 2.x, but dropped for now as of 3.0.10.
+         */
+        this.ig.custom.ignoreFiles = [];
+        this.ig.default.ignoreFiles = [];
 
         if (options.ignore !== false) {
             let ignorePath;
@@ -154,13 +169,11 @@ class IgnoredPaths {
                     debug(`Loaded ignore file ${ignorePath}`);
                 } catch (e) {
                     debug("Could not find ignore file in cwd");
-                    this.options = options;
                 }
             }
 
             if (ignorePath) {
                 debug(`Adding ${ignorePath}`);
-                this.baseDir = path.dirname(path.resolve(options.cwd, ignorePath));
                 this.addIgnoreFile(this.ig.custom, ignorePath);
                 this.addIgnoreFile(this.ig.default, ignorePath);
             } else {
@@ -187,8 +200,8 @@ class IgnoredPaths {
                         if (packageJSONOptions.eslintIgnore) {
                             if (Array.isArray(packageJSONOptions.eslintIgnore)) {
                                 packageJSONOptions.eslintIgnore.forEach(pattern => {
-                                    addPattern(this.ig.custom, pattern);
-                                    addPattern(this.ig.default, pattern);
+                                    this.addPatternRelativeToIgnoreFile(this.ig.custom, pattern);
+                                    this.addPatternRelativeToIgnoreFile(this.ig.default, pattern);
                                 });
                             } else {
                                 throw new TypeError("Package.json eslintIgnore property requires an array of paths");
@@ -202,12 +215,37 @@ class IgnoredPaths {
             }
 
             if (options.ignorePattern) {
-                addPattern(this.ig.custom, options.ignorePattern);
-                addPattern(this.ig.default, options.ignorePattern);
+                this.addPatternRelativeToCwd(this.ig.custom, options.ignorePattern);
+                this.addPatternRelativeToCwd(this.ig.default, options.ignorePattern);
             }
         }
+    }
 
-        this.options = options;
+    /*
+     * If `ignoreFileDir` is a subdirectory of `cwd`, all paths will be normalized to be relative to `cwd`.
+     * Otherwise, all paths will be normalized to be relative to `ignoreFileDir`.
+     * This ensures that the final normalized ignore rule will not contain `..`, which is forbidden in
+     * ignore rules.
+     */
+
+    addPatternRelativeToCwd(ig, pattern) {
+        ig.addPattern(
+            this.getBaseDir() === this.options.cwd
+                ? pattern
+                : relativize(pattern, path.relative(this.ignoreFileDir, this.options.cwd))
+        );
+    }
+
+    addPatternRelativeToIgnoreFile(ig, pattern) {
+        ig.addPattern(
+            this.getBaseDir() === this.ignoreFileDir
+                ? pattern
+                : relativize(pattern, path.relative(this.options.cwd, this.ignoreFileDir))
+        );
+    }
+
+    getBaseDir() {
+        return this.ignoreFileDir.startsWith(this.options.cwd) ? this.options.cwd : this.ignoreFileDir;
     }
 
     /**
@@ -217,7 +255,7 @@ class IgnoredPaths {
      */
     readIgnoreFile(filePath) {
         if (typeof this.cache[filePath] === "undefined") {
-            this.cache[filePath] = fs.readFileSync(filePath, "utf8");
+            this.cache[filePath] = fs.readFileSync(filePath, "utf8").split("\n").filter(Boolean);
         }
         return this.cache[filePath];
     }
@@ -226,11 +264,13 @@ class IgnoredPaths {
      * add ignore file to node-ignore instance
      * @param {Object} ig, instance of node-ignore
      * @param {string} filePath, file to add to ig
-     * @returns {array} raw ignore rules
+     * @returns {void}
      */
     addIgnoreFile(ig, filePath) {
         ig.ignoreFiles.push(filePath);
-        return ig.add(this.readIgnoreFile(filePath));
+        this
+            .readIgnoreFile(filePath)
+            .forEach(ignoreRule => this.addPatternRelativeToIgnoreFile(ig, ignoreRule));
     }
 
     /**
@@ -243,7 +283,7 @@ class IgnoredPaths {
 
         let result = false;
         const absolutePath = path.resolve(this.options.cwd, filepath);
-        const relativePath = pathUtil.getRelativePath(absolutePath, this.baseDir);
+        const relativePath = pathUtil.getRelativePath(absolutePath, this.getBaseDir());
 
         if (typeof category === "undefined") {
             result = (this.ig.default.filter([relativePath]).length === 0) ||
@@ -261,8 +301,10 @@ class IgnoredPaths {
      * @returns {function()} method to check whether a folder should be ignored by glob.
      */
     getIgnoredFoldersGlobChecker() {
+        const baseDir = this.getBaseDir();
+        const ig = ignore();
 
-        const ig = ignore().add(DEFAULT_IGNORE_DIRS);
+        DEFAULT_IGNORE_DIRS.forEach(ignoreDir => this.addPatternRelativeToCwd(ig, ignoreDir));
 
         if (this.options.dotfiles !== true) {
 
@@ -276,10 +318,8 @@ class IgnoredPaths {
 
         const filter = ig.createFilter();
 
-        const base = this.baseDir;
-
         return function(absolutePath) {
-            const relative = pathUtil.getRelativePath(absolutePath, base);
+            const relative = pathUtil.getRelativePath(absolutePath, baseDir);
 
             if (!relative) {
                 return false;
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/prefer-object-spread.js b/lib/rules/prefer-object-spread.js
index 6ddb66540..3df16b7ae 100644
--- a/lib/rules/prefer-object-spread.js
+++ b/lib/rules/prefer-object-spread.js
@@ -180,8 +180,14 @@ function defineFixer(node, sourceCode) {
                 for (const innerParen of innerParens) {
                     yield fixer.remove(innerParen);
                 }
-                yield fixer.removeRange([left.range[0], getEndWithSpaces(left, sourceCode)]);
-                yield fixer.removeRange([getStartWithSpaces(right, sourceCode), right.range[1]]);
+                const leftRange = [left.range[0], getEndWithSpaces(left, sourceCode)];
+                const rightRange = [
+                    Math.max(getStartWithSpaces(right, sourceCode), leftRange[1]), // Ensure ranges don't overlap
+                    right.range[1]
+                ];
+
+                yield fixer.removeRange(leftRange);
+                yield fixer.removeRange(rightRange);
 
                 // Remove the comma of this argument if it's duplication.
                 if (
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..e0ec296a9 100644
--- a/packages/eslint-config-eslint/default.yml
+++ b/packages/eslint-config-eslint/default.yml
@@ -54,6 +54,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 +161,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/fixtures/ignored-paths/subdir/.eslintignoreInChildDir b/tests/fixtures/ignored-paths/subdir/.eslintignoreInChildDir
new file mode 100644
index 000000000..8c7a20132
--- /dev/null
+++ b/tests/fixtures/ignored-paths/subdir/.eslintignoreInChildDir
@@ -0,0 +1,5 @@
+/undef.js
+foo.js
+*.txt
+!/bar.txt
+!baz.txt
diff --git a/tests/lib/ignored-paths.js b/tests/lib/ignored-paths.js
index 010488931..dfcf310e5 100644
--- a/tests/lib/ignored-paths.js
+++ b/tests/lib/ignored-paths.js
@@ -123,7 +123,7 @@ describe("IgnoredPaths", () => {
             const ignoredPaths = new IgnoredPaths({ ignore: true, cwd: getFixturePath() });
             const ignorePatterns = getIgnorePatterns(ignoredPaths);
 
-            assert.isNotNull(ignoredPaths.baseDir);
+            assert.isNotNull(ignoredPaths.ignoreFileDir);
             assert.deepStrictEqual(getIgnoreFiles(ignoredPaths), [expectedIgnoreFile]);
             assert.include(ignorePatterns, "sampleignorepattern");
         });
@@ -131,7 +131,7 @@ describe("IgnoredPaths", () => {
         it("should set baseDir to cwd when no ignore file was loaded", () => {
             const ignoredPaths = new IgnoredPaths({ cwd: getFixturePath("no-ignore-file") });
 
-            assert.strictEqual(ignoredPaths.baseDir, getFixturePath("no-ignore-file"));
+            assert.strictEqual(ignoredPaths.ignoreFileDir, getFixturePath("no-ignore-file"));
         });
 
         it("should not travel to parent directories to find .eslintignore when it's missing and cwd is provided", () => {
@@ -241,7 +241,7 @@ describe("IgnoredPaths", () => {
         it("should set baseDir to directory containing ignorePath if provided", () => {
             const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: ignoreFilePath, cwd: getFixturePath() });
 
-            assert.strictEqual(ignoredPaths.baseDir, path.dirname(ignoreFilePath));
+            assert.strictEqual(ignoredPaths.ignoreFileDir, path.dirname(ignoreFilePath));
         });
 
     });
@@ -401,6 +401,40 @@ describe("IgnoredPaths", () => {
             assert.isFalse(ignoredPaths.contains(getFixturePath("subdir/undef.js")));
             assert.isTrue(ignoredPaths.contains(getFixturePath("undef.js")));
         });
+
+        it("should resolve relative paths from the ignorePath when it's in a child directory", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() });
+
+            assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/undef.js")));
+            assert.isFalse(ignoredPaths.contains(getFixturePath("undef.js")));
+            assert.isTrue(ignoredPaths.contains(getFixturePath("foo.js")));
+            assert.isTrue(ignoredPaths.contains(getFixturePath("subdir/foo.js")));
+
+            assert.isTrue(ignoredPaths.contains(getFixturePath("node_modules/bar.js")));
+        });
+
+        it("should resolve relative paths from the ignorePath when it contains negated globs", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() });
+
+            assert.isTrue(ignoredPaths.contains("subdir/blah.txt"));
+            assert.isTrue(ignoredPaths.contains("blah.txt"));
+            assert.isFalse(ignoredPaths.contains("subdir/bar.txt"));
+            assert.isTrue(ignoredPaths.contains("bar.txt"));
+            assert.isFalse(ignoredPaths.contains("subdir/baz.txt"));
+            assert.isFalse(ignoredPaths.contains("baz.txt"));
+        });
+
+        it("should resolve default ignore patterns from the CWD even when the ignorePath is in a subdirectory", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath("subdir/.eslintignoreInChildDir"), cwd: getFixturePath() });
+
+            assert.isTrue(ignoredPaths.contains("node_modules/blah.js"));
+        });
+
+        it("should resolve default ignore patterns from the CWD even when the ignorePath is in a parent directory", () => {
+            const ignoredPaths = new IgnoredPaths({ ignore: true, ignorePath: getFixturePath(".eslintignoreForDifferentCwd"), cwd: getFixturePath("subdir") });
+
+            assert.isTrue(ignoredPaths.contains("node_modules/blah.js"));
+        });
     });
 
     describe("initialization with ignorePath containing commented lines", () => {
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/prefer-object-spread.js b/tests/lib/rules/prefer-object-spread.js
index 7790f8796..acac665e5 100644
--- a/tests/lib/rules/prefer-object-spread.js
+++ b/tests/lib/rules/prefer-object-spread.js
@@ -808,6 +808,32 @@ ruleTester.run("prefer-object-spread", rule, {
                     column: 29
                 }
             ]
+        },
+
+        // https://github.com/eslint/eslint/issues/10646
+        {
+            code: "Object.assign({ });",
+            output: "({});",
+            errors: [
+                {
+                    messageId: "useLiteralMessage",
+                    type: "CallExpression",
+                    line: 1,
+                    column: 1
+                }
+            ]
+        },
+        {
+            code: "Object.assign({\n});",
+            output: "({});",
+            errors: [
+                {
+                    messageId: "useLiteralMessage",
+                    type: "CallExpression",
+                    line: 1,
+                    column: 1
+                }
+            ]
         }
     ]
 });
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]
+        }
+    ]
+});