require-atomic-updates.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /**
  2. * @fileoverview disallow assignments that can lead to race conditions due to usage of `await` or `yield`
  3. * @author Teddy Katz
  4. * @author Toru Nagashima
  5. */
  6. "use strict";
  7. /**
  8. * Make the map from identifiers to each reference.
  9. * @param {escope.Scope} scope The scope to get references.
  10. * @param {Map<Identifier, escope.Reference>} [outReferenceMap] The map from identifier nodes to each reference object.
  11. * @returns {Map<Identifier, escope.Reference>} `referenceMap`.
  12. */
  13. function createReferenceMap(scope, outReferenceMap = new Map()) {
  14. for (const reference of scope.references) {
  15. if (reference.resolved === null) {
  16. continue;
  17. }
  18. outReferenceMap.set(reference.identifier, reference);
  19. }
  20. for (const childScope of scope.childScopes) {
  21. if (childScope.type !== "function") {
  22. createReferenceMap(childScope, outReferenceMap);
  23. }
  24. }
  25. return outReferenceMap;
  26. }
  27. /**
  28. * Get `reference.writeExpr` of a given reference.
  29. * If it's the read reference of MemberExpression in LHS, returns RHS in order to address `a.b = await a`
  30. * @param {escope.Reference} reference The reference to get.
  31. * @returns {Expression|null} The `reference.writeExpr`.
  32. */
  33. function getWriteExpr(reference) {
  34. if (reference.writeExpr) {
  35. return reference.writeExpr;
  36. }
  37. let node = reference.identifier;
  38. while (node) {
  39. const t = node.parent.type;
  40. if (t === "AssignmentExpression" && node.parent.left === node) {
  41. return node.parent.right;
  42. }
  43. if (t === "MemberExpression" && node.parent.object === node) {
  44. node = node.parent;
  45. continue;
  46. }
  47. break;
  48. }
  49. return null;
  50. }
  51. /**
  52. * Checks if an expression is a variable that can only be observed within the given function.
  53. * @param {Variable|null} variable The variable to check
  54. * @param {boolean} isMemberAccess If `true` then this is a member access.
  55. * @returns {boolean} `true` if the variable is local to the given function, and is never referenced in a closure.
  56. */
  57. function isLocalVariableWithoutEscape(variable, isMemberAccess) {
  58. if (!variable) {
  59. return false; // A global variable which was not defined.
  60. }
  61. // If the reference is a property access and the variable is a parameter, it handles the variable is not local.
  62. if (isMemberAccess && variable.defs.some(d => d.type === "Parameter")) {
  63. return false;
  64. }
  65. const functionScope = variable.scope.variableScope;
  66. return variable.references.every(
  67. reference => reference.from.variableScope === functionScope,
  68. );
  69. }
  70. /**
  71. * Represents segment information.
  72. */
  73. class SegmentInfo {
  74. constructor() {
  75. this.info = new WeakMap();
  76. }
  77. /**
  78. * Initialize the segment information.
  79. * @param {PathSegment} segment The segment to initialize.
  80. * @returns {void}
  81. */
  82. initialize(segment) {
  83. const outdatedReadVariables = new Set();
  84. const freshReadVariables = new Set();
  85. for (const prevSegment of segment.prevSegments) {
  86. const info = this.info.get(prevSegment);
  87. if (info) {
  88. info.outdatedReadVariables.forEach(
  89. Set.prototype.add,
  90. outdatedReadVariables,
  91. );
  92. info.freshReadVariables.forEach(
  93. Set.prototype.add,
  94. freshReadVariables,
  95. );
  96. }
  97. }
  98. this.info.set(segment, { outdatedReadVariables, freshReadVariables });
  99. }
  100. /**
  101. * Mark a given variable as read on given segments.
  102. * @param {PathSegment[]} segments The segments that it read the variable on.
  103. * @param {Variable} variable The variable to be read.
  104. * @returns {void}
  105. */
  106. markAsRead(segments, variable) {
  107. for (const segment of segments) {
  108. const info = this.info.get(segment);
  109. if (info) {
  110. info.freshReadVariables.add(variable);
  111. // If a variable is freshly read again, then it's no more out-dated.
  112. info.outdatedReadVariables.delete(variable);
  113. }
  114. }
  115. }
  116. /**
  117. * Move `freshReadVariables` to `outdatedReadVariables`.
  118. * @param {PathSegment[]} segments The segments to process.
  119. * @returns {void}
  120. */
  121. makeOutdated(segments) {
  122. for (const segment of segments) {
  123. const info = this.info.get(segment);
  124. if (info) {
  125. info.freshReadVariables.forEach(
  126. Set.prototype.add,
  127. info.outdatedReadVariables,
  128. );
  129. info.freshReadVariables.clear();
  130. }
  131. }
  132. }
  133. /**
  134. * Check if a given variable is outdated on the current segments.
  135. * @param {PathSegment[]} segments The current segments.
  136. * @param {Variable} variable The variable to check.
  137. * @returns {boolean} `true` if the variable is outdated on the segments.
  138. */
  139. isOutdated(segments, variable) {
  140. for (const segment of segments) {
  141. const info = this.info.get(segment);
  142. if (info && info.outdatedReadVariables.has(variable)) {
  143. return true;
  144. }
  145. }
  146. return false;
  147. }
  148. }
  149. //------------------------------------------------------------------------------
  150. // Rule Definition
  151. //------------------------------------------------------------------------------
  152. /** @type {import('../types').Rule.RuleModule} */
  153. module.exports = {
  154. meta: {
  155. type: "problem",
  156. defaultOptions: [
  157. {
  158. allowProperties: false,
  159. },
  160. ],
  161. docs: {
  162. description:
  163. "Disallow assignments that can lead to race conditions due to usage of `await` or `yield`",
  164. recommended: false,
  165. url: "https://eslint.org/docs/latest/rules/require-atomic-updates",
  166. },
  167. fixable: null,
  168. schema: [
  169. {
  170. type: "object",
  171. properties: {
  172. allowProperties: {
  173. type: "boolean",
  174. },
  175. },
  176. additionalProperties: false,
  177. },
  178. ],
  179. messages: {
  180. nonAtomicUpdate:
  181. "Possible race condition: `{{value}}` might be reassigned based on an outdated value of `{{value}}`.",
  182. nonAtomicObjectUpdate:
  183. "Possible race condition: `{{value}}` might be assigned based on an outdated state of `{{object}}`.",
  184. },
  185. },
  186. create(context) {
  187. const [{ allowProperties }] = context.options;
  188. const sourceCode = context.sourceCode;
  189. const assignmentReferences = new Map();
  190. const segmentInfo = new SegmentInfo();
  191. let stack = null;
  192. return {
  193. onCodePathStart(codePath, node) {
  194. const scope = sourceCode.getScope(node);
  195. const shouldVerify =
  196. scope.type === "function" &&
  197. (scope.block.async || scope.block.generator);
  198. stack = {
  199. upper: stack,
  200. codePath,
  201. referenceMap: shouldVerify
  202. ? createReferenceMap(scope)
  203. : null,
  204. currentSegments: new Set(),
  205. };
  206. },
  207. onCodePathEnd() {
  208. stack = stack.upper;
  209. },
  210. // Initialize the segment information.
  211. onCodePathSegmentStart(segment) {
  212. segmentInfo.initialize(segment);
  213. stack.currentSegments.add(segment);
  214. },
  215. onUnreachableCodePathSegmentStart(segment) {
  216. stack.currentSegments.add(segment);
  217. },
  218. onUnreachableCodePathSegmentEnd(segment) {
  219. stack.currentSegments.delete(segment);
  220. },
  221. onCodePathSegmentEnd(segment) {
  222. stack.currentSegments.delete(segment);
  223. },
  224. // Handle references to prepare verification.
  225. Identifier(node) {
  226. const { referenceMap } = stack;
  227. const reference = referenceMap && referenceMap.get(node);
  228. // Ignore if this is not a valid variable reference.
  229. if (!reference) {
  230. return;
  231. }
  232. const variable = reference.resolved;
  233. const writeExpr = getWriteExpr(reference);
  234. const isMemberAccess =
  235. reference.identifier.parent.type === "MemberExpression";
  236. // Add a fresh read variable.
  237. if (
  238. reference.isRead() &&
  239. !(writeExpr && writeExpr.parent.operator === "=")
  240. ) {
  241. segmentInfo.markAsRead(stack.currentSegments, variable);
  242. }
  243. /*
  244. * Register the variable to verify after ESLint traversed the `writeExpr` node
  245. * if this reference is an assignment to a variable which is referred from other closure.
  246. */
  247. if (
  248. writeExpr &&
  249. writeExpr.parent.right === writeExpr && // ← exclude variable declarations.
  250. !isLocalVariableWithoutEscape(variable, isMemberAccess)
  251. ) {
  252. let refs = assignmentReferences.get(writeExpr);
  253. if (!refs) {
  254. refs = [];
  255. assignmentReferences.set(writeExpr, refs);
  256. }
  257. refs.push(reference);
  258. }
  259. },
  260. /*
  261. * Verify assignments.
  262. * If the reference exists in `outdatedReadVariables` list, report it.
  263. */
  264. ":expression:exit"(node) {
  265. // referenceMap exists if this is in a resumable function scope.
  266. if (!stack.referenceMap) {
  267. return;
  268. }
  269. // Mark the read variables on this code path as outdated.
  270. if (
  271. node.type === "AwaitExpression" ||
  272. node.type === "YieldExpression"
  273. ) {
  274. segmentInfo.makeOutdated(stack.currentSegments);
  275. }
  276. // Verify.
  277. const references = assignmentReferences.get(node);
  278. if (references) {
  279. assignmentReferences.delete(node);
  280. for (const reference of references) {
  281. const variable = reference.resolved;
  282. if (
  283. segmentInfo.isOutdated(
  284. stack.currentSegments,
  285. variable,
  286. )
  287. ) {
  288. if (node.parent.left === reference.identifier) {
  289. context.report({
  290. node: node.parent,
  291. messageId: "nonAtomicUpdate",
  292. data: {
  293. value: variable.name,
  294. },
  295. });
  296. } else if (!allowProperties) {
  297. context.report({
  298. node: node.parent,
  299. messageId: "nonAtomicObjectUpdate",
  300. data: {
  301. value: sourceCode.getText(
  302. node.parent.left,
  303. ),
  304. object: variable.name,
  305. },
  306. });
  307. }
  308. }
  309. }
  310. }
  311. },
  312. };
  313. },
  314. };