You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

240 lines
9.7 KiB

  1. /**
  2. * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
  3. * @author Teddy Katz
  4. */
  5. "use strict";
  6. const astUtils = require("../util/ast-utils");
  7. //------------------------------------------------------------------------------
  8. // Rule Definition
  9. //------------------------------------------------------------------------------
  10. module.exports = {
  11. meta: {
  12. docs: {
  13. description: "disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
  14. category: "Possible Errors",
  15. recommended: false,
  16. url: "https://eslint.org/docs/rules/require-atomic-updates"
  17. },
  18. fixable: null,
  19. schema: [],
  20. messages: {
  21. nonAtomicUpdate: "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`."
  22. }
  23. },
  24. create(context) {
  25. const sourceCode = context.getSourceCode();
  26. const identifierToSurroundingFunctionMap = new WeakMap();
  27. const expressionsByCodePathSegment = new Map();
  28. //----------------------------------------------------------------------
  29. // Helpers
  30. //----------------------------------------------------------------------
  31. const resolvedVariableCache = new WeakMap();
  32. /**
  33. * Gets the variable scope around this variable reference
  34. * @param {ASTNode} identifier An `Identifier` AST node
  35. * @returns {Scope|null} An escope Scope
  36. */
  37. function getScope(identifier) {
  38. for (let currentNode = identifier; currentNode; currentNode = currentNode.parent) {
  39. const scope = sourceCode.scopeManager.acquire(currentNode, true);
  40. if (scope) {
  41. return scope;
  42. }
  43. }
  44. return null;
  45. }
  46. /**
  47. * Resolves a given identifier to a given scope
  48. * @param {ASTNode} identifier An `Identifier` AST node
  49. * @param {Scope} scope An escope Scope
  50. * @returns {Variable|null} An escope Variable corresponding to the given identifier
  51. */
  52. function resolveVariableInScope(identifier, scope) {
  53. return scope.variables.find(variable => variable.name === identifier.name) ||
  54. (scope.upper ? resolveVariableInScope(identifier, scope.upper) : null);
  55. }
  56. /**
  57. * Resolves an identifier to a variable
  58. * @param {ASTNode} identifier An identifier node
  59. * @returns {Variable|null} The escope Variable that uses this identifier
  60. */
  61. function resolveVariable(identifier) {
  62. if (!resolvedVariableCache.has(identifier)) {
  63. const surroundingScope = getScope(identifier);
  64. if (surroundingScope) {
  65. resolvedVariableCache.set(identifier, resolveVariableInScope(identifier, surroundingScope));
  66. } else {
  67. resolvedVariableCache.set(identifier, null);
  68. }
  69. }
  70. return resolvedVariableCache.get(identifier);
  71. }
  72. /**
  73. * Checks if an expression is a variable that can only be observed within the given function.
  74. * @param {ASTNode} expression The expression to check
  75. * @param {ASTNode} surroundingFunction The function node
  76. * @returns {boolean} `true` if the expression is a variable which is local to the given function, and is never
  77. * referenced in a closure.
  78. */
  79. function isLocalVariableWithoutEscape(expression, surroundingFunction) {
  80. if (expression.type !== "Identifier") {
  81. return false;
  82. }
  83. const variable = resolveVariable(expression);
  84. if (!variable) {
  85. return false;
  86. }
  87. return variable.references.every(reference => identifierToSurroundingFunctionMap.get(reference.identifier) === surroundingFunction) &&
  88. variable.defs.every(def => identifierToSurroundingFunctionMap.get(def.name) === surroundingFunction);
  89. }
  90. /**
  91. * Reports an AssignmentExpression node that has a non-atomic update
  92. * @param {ASTNode} assignmentExpression The assignment that is potentially unsafe
  93. * @returns {void}
  94. */
  95. function reportAssignment(assignmentExpression) {
  96. context.report({
  97. node: assignmentExpression,
  98. messageId: "nonAtomicUpdate",
  99. data: {
  100. value: sourceCode.getText(assignmentExpression.left)
  101. }
  102. });
  103. }
  104. /**
  105. * If the control flow graph of a function enters an assignment expression, then does the
  106. * both of the following steps in order (possibly with other steps in between) before exiting the
  107. * assignment expression, then the assignment might be using an outdated value.
  108. * 1. Enters a read of the variable or property assigned in the expression (not necessary if operator assignment is used)
  109. * 2. Exits an `await` or `yield` expression
  110. *
  111. * This function checks for the outdated values and reports them.
  112. * @param {CodePathSegment} codePathSegment The current code path segment to traverse
  113. * @param {ASTNode} surroundingFunction The function node containing the code path segment
  114. * @returns {void}
  115. */
  116. function findOutdatedReads(
  117. codePathSegment,
  118. surroundingFunction,
  119. {
  120. seenSegments = new Set(),
  121. openAssignmentsWithoutReads = new Set(),
  122. openAssignmentsWithReads = new Set()
  123. } = {}
  124. ) {
  125. if (seenSegments.has(codePathSegment)) {
  126. // An AssignmentExpression can't contain loops, so it's not necessary to reenter them with new state.
  127. return;
  128. }
  129. expressionsByCodePathSegment.get(codePathSegment).forEach(({ entering, node }) => {
  130. if (node.type === "AssignmentExpression") {
  131. if (entering) {
  132. (node.operator === "=" ? openAssignmentsWithoutReads : openAssignmentsWithReads).add(node);
  133. } else {
  134. openAssignmentsWithoutReads.delete(node);
  135. openAssignmentsWithReads.delete(node);
  136. }
  137. } else if (!entering && (node.type === "AwaitExpression" || node.type === "YieldExpression")) {
  138. [...openAssignmentsWithReads]
  139. .filter(assignment => !isLocalVariableWithoutEscape(assignment.left, surroundingFunction))
  140. .forEach(reportAssignment);
  141. openAssignmentsWithReads.clear();
  142. } else if (!entering && (node.type === "Identifier" || node.type === "MemberExpression")) {
  143. [...openAssignmentsWithoutReads]
  144. .filter(assignment => (
  145. assignment.left !== node &&
  146. assignment.left.type === node.type &&
  147. astUtils.equalTokens(assignment.left, node, sourceCode)
  148. ))
  149. .forEach(assignment => {
  150. openAssignmentsWithoutReads.delete(assignment);
  151. openAssignmentsWithReads.add(assignment);
  152. });
  153. }
  154. });
  155. codePathSegment.nextSegments.forEach(nextSegment => {
  156. findOutdatedReads(
  157. nextSegment,
  158. surroundingFunction,
  159. {
  160. seenSegments: new Set(seenSegments).add(codePathSegment),
  161. openAssignmentsWithoutReads: new Set(openAssignmentsWithoutReads),
  162. openAssignmentsWithReads: new Set(openAssignmentsWithReads)
  163. }
  164. );
  165. });
  166. }
  167. //----------------------------------------------------------------------
  168. // Public
  169. //----------------------------------------------------------------------
  170. const currentCodePathSegmentStack = [];
  171. let currentCodePathSegment = null;
  172. const functionStack = [];
  173. return {
  174. onCodePathStart() {
  175. currentCodePathSegmentStack.push(currentCodePathSegment);
  176. },
  177. onCodePathEnd(codePath, node) {
  178. currentCodePathSegment = currentCodePathSegmentStack.pop();
  179. if (astUtils.isFunction(node) && (node.async || node.generator)) {
  180. findOutdatedReads(codePath.initialSegment, node);
  181. }
  182. },
  183. onCodePathSegmentStart(segment) {
  184. currentCodePathSegment = segment;
  185. expressionsByCodePathSegment.set(segment, []);
  186. },
  187. "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression"(node) {
  188. expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: true, node });
  189. },
  190. "AssignmentExpression, Identifier, MemberExpression, AwaitExpression, YieldExpression:exit"(node) {
  191. expressionsByCodePathSegment.get(currentCodePathSegment).push({ entering: false, node });
  192. },
  193. ":function"(node) {
  194. functionStack.push(node);
  195. },
  196. ":function:exit"() {
  197. functionStack.pop();
  198. },
  199. Identifier(node) {
  200. if (functionStack.length) {
  201. identifierToSurroundingFunctionMap.set(node, functionStack[functionStack.length - 1]);
  202. }
  203. }
  204. };
  205. }
  206. };