esupgrade 2025.16.0 → 2025.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.github/agents/superjoe.agent.md +1 -1
  2. package/.github/copilot-instructions.md +3 -3
  3. package/.pre-commit-config.yaml +1 -1
  4. package/README.md +57 -9
  5. package/package.json +1 -1
  6. package/src/jQuery/nextToNextElementSibling.js +1 -2
  7. package/src/jQuery/prevToPreviousElementSibling.js +1 -2
  8. package/src/jQuery/readyToDOMContentLoaded.js +1 -2
  9. package/src/jQuery/siblingsToSiblingsArray.js +1 -2
  10. package/src/types.js +4 -5
  11. package/src/widelyAvailable/argumentsToRestParameters.js +1 -2
  12. package/src/widelyAvailable/arrayConcatToSpread.js +1 -3
  13. package/src/widelyAvailable/arrayFilterToFind.js +1 -3
  14. package/src/widelyAvailable/arrayFromForEachToForOf.js +1 -2
  15. package/src/widelyAvailable/arraySliceToSpread.js +1 -3
  16. package/src/widelyAvailable/consoleLogToInfo.js +2 -5
  17. package/src/widelyAvailable/constructorToClass.js +2 -4
  18. package/src/widelyAvailable/defaultParameterValues.js +1 -3
  19. package/src/widelyAvailable/forLoopToForOf.js +1 -3
  20. package/src/widelyAvailable/globalContextToGlobalThis.js +1 -2
  21. package/src/widelyAvailable/indexOfToIncludes.js +1 -3
  22. package/src/widelyAvailable/indexOfToStartsWith.js +1 -3
  23. package/src/widelyAvailable/iterableForEachToForOf.js +1 -2
  24. package/src/widelyAvailable/lastIndexOfToEndsWith.js +1 -3
  25. package/src/widelyAvailable/namedArrowFunctionToNamedFunction.js +1 -3
  26. package/src/widelyAvailable/nullishCoalescingOperator.js +1 -3
  27. package/src/widelyAvailable/objectKeysForEachToEntries.js +1 -3
  28. package/src/widelyAvailable/objectKeysMapToValues.js +119 -0
  29. package/src/widelyAvailable/objectPropertyExtractionToDestructuring.js +342 -0
  30. package/src/widelyAvailable/removeUseStrictFromModules.js +1 -3
  31. package/src/widelyAvailable/substrToSlice.js +1 -3
  32. package/src/widelyAvailable/substringToStartsWith.js +1 -3
  33. package/src/widelyAvailable.js +2 -0
  34. package/tests/widelyAvailable/object-keys-map-to-values.test.js +109 -0
  35. package/tests/widelyAvailable/object-property-extraction-to-destructuring.test.js +314 -0
@@ -12,7 +12,7 @@ description: CodingJoe's digital clone following his coding guidelines and best
12
12
 
13
13
  You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
14
14
  ```console
15
- curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | cat
15
+ curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | head -n 500
16
16
  ```
17
17
 
18
18
  You MUST ALWAYS read the `CONTRIBUTING.md` file before planning or writing any code.
@@ -1,15 +1,15 @@
1
+ YOUR PRIORITY IS TO ENSURE THE SAFETY OF ALL TRANSFORMATIONS.
2
+
1
3
  When writing code, you MUST ALWAYS follow the [naming guidelines](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md).
2
4
 
3
5
  Usage:
4
6
 
5
7
  ```console
6
- curl -sL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md
8
+ curl -sL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | head -n 500
7
9
  ```
8
10
 
9
11
  All code must be fully tested with a 100% coverage. Unreachable code must be removed.
10
12
  Follow the [CONTRIBUTING.md](./CONTRIBUTING.md) guidelines for testing and linting.
11
- The test suite will fail if the line coverage is below 100%.
12
- The test coverage will be written to stdout when running the tests via `npm test | grep -v '100.00'`.
13
13
 
14
14
  All transformers must be documented in the README.md.
15
15
 
@@ -32,7 +32,7 @@ repos:
32
32
  - id: write-good
33
33
  args: [--no-passive]
34
34
  - repo: https://github.com/pre-commit/mirrors-eslint
35
- rev: v10.3.0
35
+ rev: v10.4.0
36
36
  hooks:
37
37
  - id: eslint
38
38
  args: ["--fix"]
package/README.md CHANGED
@@ -47,9 +47,9 @@ npx esupgrade --help
47
47
  ```
48
48
 
49
49
  <picture>
50
- <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark-dark.svg">
51
- <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark.svg">
52
- <img alt="Baseline: widely available" src="https://web-platform-dx.github.io/web-features/assets/img/baseline-wordmark.svg" height="32" align="right">
50
+ <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark-dark.svg">
51
+ <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-wordmark.svg">
52
+ <img alt="Baseline: widely available" src="https://web-platform-dx.github.io/assets/img/baseline-wordmark.svg" height="32" align="right">
53
53
  </picture>
54
54
 
55
55
  ## Browser Support & Baseline
@@ -78,9 +78,9 @@ For more information about Baseline browser support, visit [web.dev/baseline][ba
78
78
  ## Transformations
79
79
 
80
80
  <picture>
81
- <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-word-dark.svg">
82
- <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-word.svg">
83
- <img alt="Baseline: widely available" src="https://web-platform-dx.github.io/web-features/assets/img/baseline-widely-word.svg" height="32" align="right">
81
+ <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-widely-word-dark.svg">
82
+ <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-widely-word.svg">
83
+ <img alt="Baseline: widely available" src="https://web-platform-dx.github.io/assets/img/baseline-widely-word.svg" height="32" align="right">
84
84
  </picture>
85
85
 
86
86
  ### Widely available
@@ -457,6 +457,21 @@ Transforms when:
457
457
  - The first statement in the callback assigns `obj[key]` to a variable
458
458
  - The object being accessed matches the object passed to Object.keys()
459
459
 
460
+ #### `Object.keys().map()` → [Object.values()][mdn-object-values]
461
+
462
+ ```diff
463
+ -Object.keys(obj).map(key => obj[key])
464
+ +Object.values(obj)
465
+ ```
466
+
467
+ Transforms `Object.keys(obj).map(key => obj[key])` patterns to use `Object.values()` directly. This eliminates redundant property lookups and expresses intent more clearly.
468
+
469
+ Transforms when:
470
+
471
+ - The callback has exactly one parameter (the key)
472
+ - The callback body returns `obj[key]` (expression or block with a single return statement)
473
+ - The object being accessed matches the object passed to Object.keys()
474
+
460
475
  #### `indexOf()` prefix check → [String.startsWith()][mdn-startswith]
461
476
 
462
477
  ```diff
@@ -535,6 +550,37 @@ The transformer handles cases where `Array.from(arguments)` has already been con
535
550
 
536
551
  Note: The `x = x || defaultValue` pattern is NOT transformed as it has different semantics (triggers on any falsy value, instead of `undefined`).
537
552
 
553
+ #### Manual property extraction → [Destructuring parameters][mdn-destructuring]
554
+
555
+ ```diff
556
+ -function fn(obj) {
557
+ - const x = obj.x;
558
+ - const y = obj.y;
559
+ - // use x and y
560
+ -}
561
+ +function fn({x, y}) {
562
+ + // use x and y
563
+ +}
564
+ ```
565
+
566
+ Transforms functions where the body begins by extracting properties from a parameter into object destructuring in the parameter list. Aliased extractions use longhand syntax:
567
+
568
+ ```diff
569
+ -function fn(obj) {
570
+ - const myX = obj.x;
571
+ -}
572
+ +function fn({x: myX}) {
573
+ +}
574
+ ```
575
+
576
+ Transforms when:
577
+
578
+ - The parameter is a simple identifier (not already destructured or a rest parameter)
579
+ - Leading statements are variable declarations extracting non-computed properties from that parameter
580
+ - The original parameter identifier is not referenced after the extraction zone
581
+
582
+ TypeScript type annotations on the original parameter are preserved on the resulting destructuring pattern.
583
+
538
584
  #### Promise chains → [async/await][mdn-async-await]
539
585
 
540
586
  ```diff
@@ -561,9 +607,9 @@ Note: The `x = x || defaultValue` pattern is NOT transformed as it has different
561
607
  - The expression is a known promise (`fetch()`, `new Promise()`, or promise methods)
562
608
 
563
609
  <picture>
564
- <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word-dark.svg">
565
- <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word.svg">
566
- <img alt="Baseline: Newly available" src="https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word.svg" height="32" align="right">
610
+ <source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word-dark.svg">
611
+ <source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg">
612
+ <img alt="Baseline: Newly available" src="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg" height="32" align="right">
567
613
  </picture>
568
614
 
569
615
  ### Newly available
@@ -794,6 +840,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
794
840
  [mdn-console]: https://developer.mozilla.org/en-US/docs/Web/API/console
795
841
  [mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
796
842
  [mdn-default-parameters]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
843
+ [mdn-destructuring]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
797
844
  [mdn-endswith]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
798
845
  [mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
799
846
  [mdn-find]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
@@ -804,6 +851,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
804
851
  [mdn-let]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
805
852
  [mdn-nullish-coalescing]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
806
853
  [mdn-object-entries]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
854
+ [mdn-object-values]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
807
855
  [mdn-promise-try]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try
808
856
  [mdn-rest-parameters]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
809
857
  [mdn-slice]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.16.0",
3
+ "version": "2025.18.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -11,8 +11,7 @@ export function nextToNextElementSibling(root) {
11
11
  let modified = false
12
12
  root
13
13
  .find(j.CallExpression)
14
- .filter((path) => {
15
- const node = path.node
14
+ .filter(({ node }) => {
16
15
  if (!j.MemberExpression.check(node.callee)) return false
17
16
  if (!j.Identifier.check(node.callee.property)) return false
18
17
  if (node.callee.property.name !== "next") return false
@@ -11,8 +11,7 @@ export function prevToPreviousElementSibling(root) {
11
11
  let modified = false
12
12
  root
13
13
  .find(j.CallExpression)
14
- .filter((path) => {
15
- const node = path.node
14
+ .filter(({ node }) => {
16
15
  if (!j.MemberExpression.check(node.callee)) return false
17
16
  if (!j.Identifier.check(node.callee.property)) return false
18
17
  if (node.callee.property.name !== "prev") return false
@@ -12,8 +12,7 @@ export function readyToDOMContentLoaded(root) {
12
12
  .find(j.CallExpression, {
13
13
  callee: { type: "MemberExpression" },
14
14
  })
15
- .filter((path) => {
16
- const node = path.node
15
+ .filter(({ node }) => {
17
16
  const callee = node.callee
18
17
  if (!j.Identifier.check(callee.property)) return false
19
18
  if (callee.property.name !== "ready") return false
@@ -11,8 +11,7 @@ export function siblingsToSiblingsArray(root) {
11
11
  let modified = false
12
12
  root
13
13
  .find(j.CallExpression)
14
- .filter((path) => {
15
- const node = path.node
14
+ .filter(({ node }) => {
16
15
  if (!j.MemberExpression.check(node.callee)) return false
17
16
  if (!j.Identifier.check(node.callee.property)) return false
18
17
  if (node.callee.property.name !== "siblings") return false
package/src/types.js CHANGED
@@ -504,9 +504,9 @@ export class NodeTest {
504
504
  */
505
505
  hasSideEffects() {
506
506
  const paramNames = new Set(
507
- this.node.params.flatMap((param) =>
508
- Array.from(new NodeTest(param).extractIdentifiersFromPattern()),
509
- ),
507
+ this.node.params.flatMap((param) => [
508
+ ...new NodeTest(param).extractIdentifiersFromPattern(),
509
+ ]),
510
510
  )
511
511
  return this.#hasNodeSideEffects(this.node.body, paramNames)
512
512
  }
@@ -1066,8 +1066,7 @@ export function processMultipleDeclarators(root, path) {
1066
1066
  * @param {string} name - Name to check for shadowing
1067
1067
  * @returns {boolean} True if the identifier is shadowed
1068
1068
  */
1069
- export function isShadowed(path, name) {
1070
- let scope = path.scope
1069
+ export function isShadowed({ scope }, name) {
1071
1070
  while (scope) {
1072
1071
  if (scope.getBindings()[name]) {
1073
1072
  return true
@@ -24,8 +24,7 @@ export function argumentsToRestParameters(root) {
24
24
  ...root.find(j.FunctionExpression).paths(),
25
25
  ]
26
26
 
27
- functionNodes.forEach((path) => {
28
- const func = path.node
27
+ functionNodes.forEach(({ node: func }) => {
29
28
  if (
30
29
  (func.params.length > 0 &&
31
30
  j.RestElement.check(func.params[func.params.length - 1])) ||
@@ -13,9 +13,7 @@ export function arrayConcatToSpread(root) {
13
13
 
14
14
  root
15
15
  .find(j.CallExpression)
16
- .filter((path) => {
17
- const node = path.node
18
-
16
+ .filter(({ node }) => {
19
17
  // Check if this is a .concat() call
20
18
  if (
21
19
  !j.MemberExpression.check(node.callee) ||
@@ -14,9 +14,7 @@ export function arrayFilterToFind(root) {
14
14
 
15
15
  root
16
16
  .find(j.MemberExpression)
17
- .filter((path) => {
18
- const node = path.node
19
-
17
+ .filter(({ node }) => {
20
18
  // Must be computed access: expr[0]
21
19
  if (!node.computed) {
22
20
  return false
@@ -13,8 +13,7 @@ export function arrayFromForEachToForOf(root) {
13
13
 
14
14
  root
15
15
  .find(j.CallExpression)
16
- .filter((path) => {
17
- const node = path.node
16
+ .filter(({ node }) => {
18
17
  // Check if this is a forEach call
19
18
  if (
20
19
  !j.MemberExpression.check(node.callee) ||
@@ -13,9 +13,7 @@ export function arraySliceToSpread(root) {
13
13
 
14
14
  root
15
15
  .find(j.CallExpression)
16
- .filter((path) => {
17
- const node = path.node
18
-
16
+ .filter(({ node }) => {
19
17
  // Check if this is a .slice() call
20
18
  if (
21
19
  !j.MemberExpression.check(node.callee) ||
@@ -12,8 +12,7 @@ export function consoleLogToInfo(root) {
12
12
 
13
13
  root
14
14
  .find(j.CallExpression)
15
- .filter((path) => {
16
- const node = path.node
15
+ .filter(({ node }) => {
17
16
  // Check if this is a console.log() call
18
17
  if (
19
18
  !j.MemberExpression.check(node.callee) ||
@@ -27,9 +26,7 @@ export function consoleLogToInfo(root) {
27
26
 
28
27
  return true
29
28
  })
30
- .forEach((path) => {
31
- const node = path.node
32
-
29
+ .forEach(({ node }) => {
33
30
  // Replace the property name from 'log' to 'info'
34
31
  node.callee.property.name = "info"
35
32
 
@@ -61,8 +61,7 @@ export function constructorToClass(root) {
61
61
  // Pattern 1: ConstructorName.prototype.methodName = ...
62
62
  root
63
63
  .find(j.ExpressionStatement)
64
- .filter((path) => {
65
- const node = path.node
64
+ .filter(({ node }) => {
66
65
  if (!j.AssignmentExpression.check(node.expression)) {
67
66
  return false
68
67
  }
@@ -105,8 +104,7 @@ export function constructorToClass(root) {
105
104
  // Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
106
105
  root
107
106
  .find(j.ExpressionStatement)
108
- .filter((path) => {
109
- const node = path.node
107
+ .filter(({ node }) => {
110
108
  if (!j.AssignmentExpression.check(node.expression)) {
111
109
  return false
112
110
  }
@@ -27,9 +27,7 @@ export function defaultParameterValues(root) {
27
27
  ...root.find(j.ArrowFunctionExpression).paths(),
28
28
  ]
29
29
 
30
- functionNodes.forEach((path) => {
31
- const func = path.node
32
-
30
+ functionNodes.forEach(({ node: func }) => {
33
31
  // Skip if function body is not a block statement
34
32
  if (!j.BlockStatement.check(func.body)) {
35
33
  return
@@ -14,9 +14,7 @@ export function forLoopToForOf(root) {
14
14
 
15
15
  root
16
16
  .find(j.ForStatement)
17
- .filter((path) => {
18
- const node = path.node
19
-
17
+ .filter(({ node }) => {
20
18
  // Check init: must be let/const i = 0
21
19
  if (!j.VariableDeclaration.check(node.init)) {
22
20
  return false
@@ -14,8 +14,7 @@ export function globalContextToGlobalThis(root) {
14
14
 
15
15
  root
16
16
  .find(j.CallExpression)
17
- .filter((path) => {
18
- const node = path.node
17
+ .filter(({ node }) => {
19
18
  // Check if this is a call to Function constructor
20
19
  if (
21
20
  !j.Identifier.check(node.callee) ||
@@ -16,9 +16,7 @@ export function indexOfToIncludes(root) {
16
16
 
17
17
  root
18
18
  .find(j.BinaryExpression)
19
- .filter((path) => {
20
- const node = path.node
21
-
19
+ .filter(({ node }) => {
22
20
  // Check for comparison operators: !==, ===, >, >=, <, <=
23
21
  if (!["!==", "===", ">", ">=", "<", "<="].includes(node.operator)) {
24
22
  return false
@@ -14,9 +14,7 @@ export function indexOfToStartsWith(root) {
14
14
 
15
15
  root
16
16
  .find(j.BinaryExpression)
17
- .filter((path) => {
18
- const node = path.node
19
-
17
+ .filter(({ node }) => {
20
18
  // Check for === or !== operators
21
19
  if (!["===", "!=="].includes(node.operator)) {
22
20
  return false
@@ -31,8 +31,7 @@ export function iterableForEachToForOf(root) {
31
31
 
32
32
  root
33
33
  .find(j.CallExpression)
34
- .filter((path) => {
35
- const node = path.node
34
+ .filter(({ node }) => {
36
35
  // Check if this is a forEach call
37
36
  if (
38
37
  !j.MemberExpression.check(node.callee) ||
@@ -15,9 +15,7 @@ export function lastIndexOfToEndsWith(root) {
15
15
 
16
16
  root
17
17
  .find(j.BinaryExpression)
18
- .filter((path) => {
19
- const node = path.node
20
-
18
+ .filter(({ node }) => {
21
19
  // Check for === or !== operators
22
20
  if (!["===", "!=="].includes(node.operator)) {
23
21
  return false
@@ -27,9 +27,7 @@ export function namedArrowFunctionToNamedFunction(root) {
27
27
 
28
28
  root
29
29
  .find(j.VariableDeclaration)
30
- .filter((path) => {
31
- const node = path.node
32
-
30
+ .filter(({ node }) => {
33
31
  // Must have exactly one declarator
34
32
  if (node.declarations.length !== 1) {
35
33
  return false
@@ -13,9 +13,7 @@ export function nullishCoalescingOperator(root) {
13
13
 
14
14
  root
15
15
  .find(j.ConditionalExpression)
16
- .filter((path) => {
17
- const node = path.node
18
-
16
+ .filter(({ node }) => {
19
17
  // Test must be a logical AND expression
20
18
  if (!j.LogicalExpression.check(node.test) || node.test.operator !== "&&") {
21
19
  return false
@@ -15,9 +15,7 @@ export function objectKeysForEachToEntries(root) {
15
15
 
16
16
  root
17
17
  .find(j.CallExpression)
18
- .filter((path) => {
19
- const node = path.node
20
-
18
+ .filter(({ node }) => {
21
19
  // Check if this is a forEach call
22
20
  if (
23
21
  !j.MemberExpression.check(node.callee) ||
@@ -0,0 +1,119 @@
1
+ import { default as j } from "jscodeshift"
2
+ import { NodeTest } from "../types.js"
3
+
4
+ /**
5
+ * Check if a callback expression body returns `targetObject[keyName]`.
6
+ *
7
+ * @param {import("ast-types").ASTNode} body - The callback body
8
+ * @param {string} keyName - The key parameter name
9
+ * @param {import("ast-types").ASTNode} targetObject - The object to compare against
10
+ * @returns {boolean} True if the body returns the expected member expression
11
+ */
12
+ function isValueAccess(body, keyName, targetObject) {
13
+ switch (body.type) {
14
+ case "MemberExpression":
15
+ return (
16
+ body.computed === true &&
17
+ j.Identifier.check(body.property) &&
18
+ body.property.name === keyName &&
19
+ new NodeTest(body.object).isEqual(targetObject)
20
+ )
21
+ case "BlockStatement": {
22
+ if (body.body.length !== 1) {
23
+ return false
24
+ }
25
+ const stmt = body.body[0]
26
+ return (
27
+ j.ReturnStatement.check(stmt) &&
28
+ stmt.argument !== null &&
29
+ isValueAccess(stmt.argument, keyName, targetObject)
30
+ )
31
+ }
32
+ default:
33
+ return false
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Transform Object.keys(obj).map(key => obj[key]) to Object.values(obj).
39
+ * Converts patterns where Object.keys() is mapped to retrieve values from the same object
40
+ * to use Object.values() directly.
41
+ *
42
+ * @param {import("jscodeshift").Collection} root - The root AST collection
43
+ * @returns {boolean} True if code was modified
44
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
45
+ */
46
+ export function objectKeysMapToValues(root) {
47
+ let modified = false
48
+
49
+ root
50
+ .find(j.CallExpression)
51
+ .filter(({ node }) => {
52
+ if (
53
+ !j.MemberExpression.check(node.callee) ||
54
+ !j.Identifier.check(node.callee.property) ||
55
+ node.callee.property.name !== "map"
56
+ ) {
57
+ return false
58
+ }
59
+
60
+ const object = node.callee.object
61
+ if (
62
+ !j.CallExpression.check(object) ||
63
+ !j.MemberExpression.check(object.callee) ||
64
+ !j.Identifier.check(object.callee.object) ||
65
+ object.callee.object.name !== "Object" ||
66
+ !j.Identifier.check(object.callee.property) ||
67
+ object.callee.property.name !== "keys"
68
+ ) {
69
+ return false
70
+ }
71
+
72
+ if (object.arguments.length !== 1) {
73
+ return false
74
+ }
75
+
76
+ if (node.arguments.length !== 1) {
77
+ return false
78
+ }
79
+
80
+ const callback = node.arguments[0]
81
+ if (
82
+ !j.ArrowFunctionExpression.check(callback) &&
83
+ !j.FunctionExpression.check(callback)
84
+ ) {
85
+ return false
86
+ }
87
+
88
+ if (callback.async || callback.generator) {
89
+ return false
90
+ }
91
+
92
+ if (callback.params.length !== 1 || !j.Identifier.check(callback.params[0])) {
93
+ return false
94
+ }
95
+
96
+ // Only allow plain identifier targets to avoid changing evaluation count
97
+ // of expressions with side-effects (getters, function calls, etc.)
98
+ if (!j.Identifier.check(object.arguments[0])) {
99
+ return false
100
+ }
101
+
102
+ const keyName = callback.params[0].name
103
+ return isValueAccess(callback.body, keyName, object.arguments[0])
104
+ })
105
+ .forEach((path) => {
106
+ const targetObject = path.node.callee.object.arguments[0]
107
+
108
+ j(path).replaceWith(
109
+ j.callExpression(
110
+ j.memberExpression(j.identifier("Object"), j.identifier("values"), false),
111
+ [targetObject],
112
+ ),
113
+ )
114
+
115
+ modified = true
116
+ })
117
+
118
+ return modified
119
+ }