esupgrade 2025.15.0 → 2025.17.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.
- package/.github/agents/superjoe.agent.md +66 -0
- package/.github/copilot-instructions.md +3 -3
- package/.github/workflows/ci.yml +1 -1
- package/.pre-commit-config.yaml +2 -1
- package/README.md +37 -9
- package/package.json +1 -1
- package/src/types.js +51 -0
- package/src/widelyAvailable/arrayFilterToFind.js +80 -0
- package/src/widelyAvailable/objectKeysMapToValues.js +121 -0
- package/src/widelyAvailable.js +2 -0
- package/tests/widelyAvailable/array-filter-to-find.test.js +173 -0
- package/tests/widelyAvailable/object-keys-map-to-values.test.js +109 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
# For format details, see: https://gh.io/customagents/config
|
|
3
|
+
|
|
4
|
+
name: SuperJoe
|
|
5
|
+
description: CodingJoe's digital clone following his coding guidelines and best practices.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# SuperJoe
|
|
10
|
+
|
|
11
|
+
## Planning
|
|
12
|
+
|
|
13
|
+
You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
|
|
14
|
+
```console
|
|
15
|
+
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | head -n 500
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
You MUST ALWAYS read the `CONTRIBUTING.md` file before planning or writing any code.
|
|
19
|
+
You MUST ALWAYS search the documentation and amend or update it as necessary.
|
|
20
|
+
You MUST ALWAYS check for pre-commit hooks and run them before committing code.
|
|
21
|
+
You MUST ALWAYS ensure that all new code is fully tested with 100% coverage. Unreachable code branches MUST be removed.
|
|
22
|
+
|
|
23
|
+
## Writing Code
|
|
24
|
+
|
|
25
|
+
Less code is more! Use the latest language features and libraries to achieve more with less code.
|
|
26
|
+
|
|
27
|
+
Do not add new dependencies, but if you do, they must be widely adopted and well-maintained in the open-source community.
|
|
28
|
+
|
|
29
|
+
You are a strong FOSS advocate with a preference for permissive licenses like BSD or MIT.
|
|
30
|
+
|
|
31
|
+
Use generators instead of adding items to lists or arrays.
|
|
32
|
+
|
|
33
|
+
Use class syntax for all object-oriented code.
|
|
34
|
+
Use named functions instead of anonymous functions whenever possible.
|
|
35
|
+
Avoid overly complex functions. Break them into smaller functions if necessary.
|
|
36
|
+
Docstrings should be written in present tense imperative mood.
|
|
37
|
+
They must start with a capital letter and end with a period.
|
|
38
|
+
Docstrings must describe the external behavior of the function, class, or method.
|
|
39
|
+
Docstrings should avoid redundant phrases like "This function" or "This method".
|
|
40
|
+
Class docstrings must not repeat the class name or start with a verb since they don't do anything themselves.
|
|
41
|
+
Avoid code comments unless they describe behavior of 3rd party code or complex algorithms.
|
|
42
|
+
Avoid loops in favor of recursive functions or generator functions.
|
|
43
|
+
Avoid functions or other code inside functions.
|
|
44
|
+
Avoid if-statements in favor of switch/match-statements or polymorphism.
|
|
45
|
+
Do not assign names to objects which are returned in the next line.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## Python
|
|
49
|
+
|
|
50
|
+
Follow PEP 8 guidelines for code style.
|
|
51
|
+
EAFP (Easier to Ask Forgiveness than Permission) is preferred over LBYL (Look Before You Leap).
|
|
52
|
+
Use type hints for all public functions, classes, and methods.
|
|
53
|
+
Use dataclasses for simple data structures.
|
|
54
|
+
Use context managers for resource management.
|
|
55
|
+
Use list/set/dict comprehensions instead of loops for creating collections.
|
|
56
|
+
Use generators for large data sets to save memory.
|
|
57
|
+
Use the walrus operator (`:=`) for inline assignments when it improves readability.
|
|
58
|
+
|
|
59
|
+
### JavaScript
|
|
60
|
+
|
|
61
|
+
Use `#` for private methods.
|
|
62
|
+
Write docstrings with jsdoc type annotations for all functions, classes, and methods.
|
|
63
|
+
|
|
64
|
+
### TypeScript
|
|
65
|
+
|
|
66
|
+
Use `#` for private methods.
|
|
@@ -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
|
|
package/.github/workflows/ci.yml
CHANGED
|
@@ -28,7 +28,7 @@ jobs:
|
|
|
28
28
|
node-version-file: package.json
|
|
29
29
|
- run: npm ci
|
|
30
30
|
- run: node --test --experimental-test-coverage --test-reporter=spec --test-reporter=lcov --test-reporter-destination=stdout --test-reporter-destination=lcov.info
|
|
31
|
-
- uses: codecov/codecov-action@
|
|
31
|
+
- uses: codecov/codecov-action@v6
|
|
32
32
|
with:
|
|
33
33
|
token: ${{ secrets.CODECOV_TOKEN }}
|
|
34
34
|
flags: javascript
|
package/.pre-commit-config.yaml
CHANGED
|
@@ -21,6 +21,7 @@ repos:
|
|
|
21
21
|
- mdformat-footnote
|
|
22
22
|
- mdformat-gfm
|
|
23
23
|
- mdformat-gfm-alerts
|
|
24
|
+
exclude: '.github/agents/'
|
|
24
25
|
- repo: https://github.com/google/yamlfmt
|
|
25
26
|
rev: v0.21.0
|
|
26
27
|
hooks:
|
|
@@ -31,7 +32,7 @@ repos:
|
|
|
31
32
|
- id: write-good
|
|
32
33
|
args: [--no-passive]
|
|
33
34
|
- repo: https://github.com/pre-commit/mirrors-eslint
|
|
34
|
-
rev: v10.
|
|
35
|
+
rev: v10.3.0
|
|
35
36
|
hooks:
|
|
36
37
|
- id: eslint
|
|
37
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/
|
|
51
|
-
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/
|
|
52
|
-
<img alt="Baseline: widely available" src="https://web-platform-dx.github.io/
|
|
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/
|
|
82
|
-
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/
|
|
83
|
-
<img alt="Baseline: widely available" src="https://web-platform-dx.github.io/
|
|
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
|
|
@@ -224,6 +224,17 @@ Supports:
|
|
|
224
224
|
+const clone = [...Array.from(items)];
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
#### `Array.filter()[0]` → [`Array.find()`][mdn-find]
|
|
228
|
+
|
|
229
|
+
```diff
|
|
230
|
+
-const first = [1, 2, 3].filter(n => n > 1)[0];
|
|
231
|
+
+const first = [1, 2, 3].find(n => n > 1);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Transforms `filter(predicate)[0]` to the more explicit and performant `find(predicate)`, which stops at the first match instead of filtering the entire array.
|
|
235
|
+
|
|
236
|
+
Transformations are limited to when the receiver can be verified as an array (array literals, `new Array()`, or known array method chains) and `filter()` is called with exactly one argument.
|
|
237
|
+
|
|
227
238
|
#### `Math.pow()` → [Exponentiation operator \*\*][mdn-exponentiation]
|
|
228
239
|
|
|
229
240
|
```diff
|
|
@@ -446,6 +457,21 @@ Transforms when:
|
|
|
446
457
|
- The first statement in the callback assigns `obj[key]` to a variable
|
|
447
458
|
- The object being accessed matches the object passed to Object.keys()
|
|
448
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
|
+
|
|
449
475
|
#### `indexOf()` prefix check → [String.startsWith()][mdn-startswith]
|
|
450
476
|
|
|
451
477
|
```diff
|
|
@@ -550,9 +576,9 @@ Note: The `x = x || defaultValue` pattern is NOT transformed as it has different
|
|
|
550
576
|
- The expression is a known promise (`fetch()`, `new Promise()`, or promise methods)
|
|
551
577
|
|
|
552
578
|
<picture>
|
|
553
|
-
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/
|
|
554
|
-
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/
|
|
555
|
-
<img alt="Baseline: Newly available" src="https://web-platform-dx.github.io/
|
|
579
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word-dark.svg">
|
|
580
|
+
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg">
|
|
581
|
+
<img alt="Baseline: Newly available" src="https://web-platform-dx.github.io/assets/img/baseline-newly-word.svg" height="32" align="right">
|
|
556
582
|
</picture>
|
|
557
583
|
|
|
558
584
|
### Newly available
|
|
@@ -785,6 +811,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
|
|
|
785
811
|
[mdn-default-parameters]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
|
|
786
812
|
[mdn-endswith]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
|
|
787
813
|
[mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
|
|
814
|
+
[mdn-find]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
|
|
788
815
|
[mdn-for-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
|
|
789
816
|
[mdn-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions
|
|
790
817
|
[mdn-globalthis]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
|
|
@@ -792,6 +819,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
|
|
|
792
819
|
[mdn-let]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
|
|
793
820
|
[mdn-nullish-coalescing]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing
|
|
794
821
|
[mdn-object-entries]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
|
|
822
|
+
[mdn-object-values]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
|
|
795
823
|
[mdn-promise-try]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try
|
|
796
824
|
[mdn-rest-parameters]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/rest_parameters
|
|
797
825
|
[mdn-slice]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice
|
package/package.json
CHANGED
package/src/types.js
CHANGED
|
@@ -494,6 +494,57 @@ export class NodeTest {
|
|
|
494
494
|
return count
|
|
495
495
|
}
|
|
496
496
|
|
|
497
|
+
/**
|
|
498
|
+
* Determine whether an inline function has side effects.
|
|
499
|
+
* Extracts parameter names from the function node and checks the body against a
|
|
500
|
+
* strict whitelist: only literals, parameter identifiers, binary expressions, and
|
|
501
|
+
* non-computed member access on parameters are side-effect free.
|
|
502
|
+
*
|
|
503
|
+
* @returns {boolean} True if the function has side effects
|
|
504
|
+
*/
|
|
505
|
+
hasSideEffects() {
|
|
506
|
+
const paramNames = new Set(
|
|
507
|
+
this.node.params.flatMap((param) =>
|
|
508
|
+
Array.from(new NodeTest(param).extractIdentifiersFromPattern()),
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
return this.#hasNodeSideEffects(this.node.body, paramNames)
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Recursively determine if an AST node has side effects given a set of allowed
|
|
516
|
+
* identifiers. Returns false only for literals, known-parameter identifiers, binary
|
|
517
|
+
* expressions, non-computed property access on side-effect-free sub-expressions, and
|
|
518
|
+
* single-return-statement block bodies. Returns true for all other node types.
|
|
519
|
+
*
|
|
520
|
+
* @param {import("ast-types").ASTNode} node - The node to inspect
|
|
521
|
+
* @param {Set<string>} paramNames - Set of allowed identifier names
|
|
522
|
+
* @returns {boolean} True if the node has side effects
|
|
523
|
+
*/
|
|
524
|
+
#hasNodeSideEffects(node, paramNames) {
|
|
525
|
+
if (j.Literal.check(node)) return false
|
|
526
|
+
if (j.Identifier.check(node)) return !paramNames.has(node.name)
|
|
527
|
+
if (j.BinaryExpression.check(node)) {
|
|
528
|
+
return (
|
|
529
|
+
this.#hasNodeSideEffects(node.left, paramNames) ||
|
|
530
|
+
this.#hasNodeSideEffects(node.right, paramNames)
|
|
531
|
+
)
|
|
532
|
+
}
|
|
533
|
+
if (j.MemberExpression.check(node)) {
|
|
534
|
+
if (node.computed) return true
|
|
535
|
+
return this.#hasNodeSideEffects(node.object, paramNames)
|
|
536
|
+
}
|
|
537
|
+
if (j.BlockStatement.check(node)) {
|
|
538
|
+
if (node.body.length !== 1) return true
|
|
539
|
+
const [stmt] = node.body
|
|
540
|
+
return (
|
|
541
|
+
!j.ReturnStatement.check(stmt) ||
|
|
542
|
+
this.#hasNodeSideEffects(stmt.argument, paramNames)
|
|
543
|
+
)
|
|
544
|
+
}
|
|
545
|
+
return true
|
|
546
|
+
}
|
|
547
|
+
|
|
497
548
|
/**
|
|
498
549
|
* Check if node eventually chains from document.
|
|
499
550
|
*
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { default as j } from "jscodeshift"
|
|
2
|
+
import { NodeTest } from "../types.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Transform Array.filter()[0] to Array.find().
|
|
6
|
+
* Converts patterns like arr.filter(predicate)[0] to arr.find(predicate).
|
|
7
|
+
*
|
|
8
|
+
* @param {import("jscodeshift").Collection} root - The root AST collection
|
|
9
|
+
* @returns {boolean} True if code was modified
|
|
10
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
|
|
11
|
+
*/
|
|
12
|
+
export function arrayFilterToFind(root) {
|
|
13
|
+
let modified = false
|
|
14
|
+
|
|
15
|
+
root
|
|
16
|
+
.find(j.MemberExpression)
|
|
17
|
+
.filter((path) => {
|
|
18
|
+
const node = path.node
|
|
19
|
+
|
|
20
|
+
// Must be computed access: expr[0]
|
|
21
|
+
if (!node.computed) {
|
|
22
|
+
return false
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Property must be literal 0
|
|
26
|
+
if (!j.Literal.check(node.property) || node.property.value !== 0) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Object must be a .filter() call
|
|
31
|
+
if (!j.CallExpression.check(node.object)) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const filterCall = node.object
|
|
36
|
+
|
|
37
|
+
if (
|
|
38
|
+
!j.MemberExpression.check(filterCall.callee) ||
|
|
39
|
+
filterCall.callee.property.name !== "filter"
|
|
40
|
+
) {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// .filter() must have exactly one argument
|
|
45
|
+
if (filterCall.arguments.length !== 1) {
|
|
46
|
+
return false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Predicate must be an inline function whose body is proven side-effect free.
|
|
50
|
+
// Named function references are skipped because their bodies cannot be inspected.
|
|
51
|
+
const predicate = filterCall.arguments[0]
|
|
52
|
+
if (
|
|
53
|
+
!j.ArrowFunctionExpression.check(predicate) &&
|
|
54
|
+
!j.FunctionExpression.check(predicate)
|
|
55
|
+
) {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (new NodeTest(predicate).hasSideEffects()) {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Object being filtered must be a known array
|
|
64
|
+
return new NodeTest(filterCall.callee.object).hasIndexOfAndIncludes()
|
|
65
|
+
})
|
|
66
|
+
.forEach((path) => {
|
|
67
|
+
const filterCall = path.node.object
|
|
68
|
+
|
|
69
|
+
j(path).replaceWith(
|
|
70
|
+
j.callExpression(
|
|
71
|
+
j.memberExpression(filterCall.callee.object, j.identifier("find"), false),
|
|
72
|
+
filterCall.arguments,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
modified = true
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
return modified
|
|
80
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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((path) => {
|
|
52
|
+
const node = path.node
|
|
53
|
+
|
|
54
|
+
if (
|
|
55
|
+
!j.MemberExpression.check(node.callee) ||
|
|
56
|
+
!j.Identifier.check(node.callee.property) ||
|
|
57
|
+
node.callee.property.name !== "map"
|
|
58
|
+
) {
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const object = node.callee.object
|
|
63
|
+
if (
|
|
64
|
+
!j.CallExpression.check(object) ||
|
|
65
|
+
!j.MemberExpression.check(object.callee) ||
|
|
66
|
+
!j.Identifier.check(object.callee.object) ||
|
|
67
|
+
object.callee.object.name !== "Object" ||
|
|
68
|
+
!j.Identifier.check(object.callee.property) ||
|
|
69
|
+
object.callee.property.name !== "keys"
|
|
70
|
+
) {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (object.arguments.length !== 1) {
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (node.arguments.length !== 1) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const callback = node.arguments[0]
|
|
83
|
+
if (
|
|
84
|
+
!j.ArrowFunctionExpression.check(callback) &&
|
|
85
|
+
!j.FunctionExpression.check(callback)
|
|
86
|
+
) {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (callback.async || callback.generator) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (callback.params.length !== 1 || !j.Identifier.check(callback.params[0])) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Only allow plain identifier targets to avoid changing evaluation count
|
|
99
|
+
// of expressions with side-effects (getters, function calls, etc.)
|
|
100
|
+
if (!j.Identifier.check(object.arguments[0])) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const keyName = callback.params[0].name
|
|
105
|
+
return isValueAccess(callback.body, keyName, object.arguments[0])
|
|
106
|
+
})
|
|
107
|
+
.forEach((path) => {
|
|
108
|
+
const targetObject = path.node.callee.object.arguments[0]
|
|
109
|
+
|
|
110
|
+
j(path).replaceWith(
|
|
111
|
+
j.callExpression(
|
|
112
|
+
j.memberExpression(j.identifier("Object"), j.identifier("values"), false),
|
|
113
|
+
[targetObject],
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
modified = true
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
return modified
|
|
121
|
+
}
|
package/src/widelyAvailable.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { anonymousFunctionToArrow } from "./widelyAvailable/anonymousFunctionToArrow.js"
|
|
2
2
|
export { argumentsToRestParameters } from "./widelyAvailable/argumentsToRestParameters.js"
|
|
3
3
|
export { arrayConcatToSpread } from "./widelyAvailable/arrayConcatToSpread.js"
|
|
4
|
+
export { arrayFilterToFind } from "./widelyAvailable/arrayFilterToFind.js"
|
|
4
5
|
export { arrayFromForEachToForOf } from "./widelyAvailable/arrayFromForEachToForOf.js"
|
|
5
6
|
export { arrayFromToSpread } from "./widelyAvailable/arrayFromToSpread.js"
|
|
6
7
|
export { arraySliceToSpread } from "./widelyAvailable/arraySliceToSpread.js"
|
|
@@ -19,6 +20,7 @@ export { namedArrowFunctionToNamedFunction } from "./widelyAvailable/namedArrowF
|
|
|
19
20
|
export { nullishCoalescingOperator } from "./widelyAvailable/nullishCoalescingOperator.js"
|
|
20
21
|
export { objectAssignToSpread } from "./widelyAvailable/objectAssignToSpread.js"
|
|
21
22
|
export { objectKeysForEachToEntries } from "./widelyAvailable/objectKeysForEachToEntries.js"
|
|
23
|
+
export { objectKeysMapToValues } from "./widelyAvailable/objectKeysMapToValues.js"
|
|
22
24
|
export { optionalChaining } from "./widelyAvailable/optionalChaining.js"
|
|
23
25
|
export { promiseToAsyncAwait } from "./widelyAvailable/promiseToAsyncAwait.js"
|
|
24
26
|
export { removeUseStrictFromModules } from "./widelyAvailable/removeUseStrictFromModules.js"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { describe, suite, test } from "node:test"
|
|
3
|
+
import { transform } from "../../src/index.js"
|
|
4
|
+
|
|
5
|
+
suite("widely-available", () => {
|
|
6
|
+
describe("arrayFilterToFind", () => {
|
|
7
|
+
test("array literal with named function - should not transform", () => {
|
|
8
|
+
const result = transform(`const first = [1, 2, 3].filter(isPositive)[0];`)
|
|
9
|
+
|
|
10
|
+
assert(!result.modified, "skip filter()[0] with named function predicate")
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test("array literal with side effect exception - should not transform", () => {
|
|
14
|
+
const result = transform(`const first = [1, 2, 3].filter(x => {
|
|
15
|
+
throw new Error("Side effect");
|
|
16
|
+
})[0];`)
|
|
17
|
+
|
|
18
|
+
assert(!result.modified, "skip filter()[0] with named function predicate")
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test("array literal with side effect out of scope variable - should not transform", () => {
|
|
22
|
+
const result = transform(`const someList = []
|
|
23
|
+
const first = [1, 2, 3].filter(x => {
|
|
24
|
+
someList.push(x);
|
|
25
|
+
})[0];`)
|
|
26
|
+
|
|
27
|
+
assert(!result.modified, "skip filter()[0] with named function predicate")
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("predicate accessing outer scope - should not transform", () => {
|
|
31
|
+
const result = transform(`const first = [1, 2, 3].filter(x => x > threshold)[0];`)
|
|
32
|
+
|
|
33
|
+
assert(!result.modified, "skip filter()[0] when predicate accesses outer scope")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("predicate calling a function - should not transform", () => {
|
|
37
|
+
const result = transform(`const first = [1, 2, 3].filter(x => sideEffect(x))[0];`)
|
|
38
|
+
|
|
39
|
+
assert(!result.modified, "skip filter()[0] when predicate calls a function")
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("predicate using computed member access - should not transform", () => {
|
|
43
|
+
const result = transform(`const first = [1, 2, 3].filter(x => x[0] > 0)[0];`)
|
|
44
|
+
|
|
45
|
+
assert(
|
|
46
|
+
!result.modified,
|
|
47
|
+
"skip filter()[0] when predicate uses computed member access",
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("predicate with multi-statement body - should not transform", () => {
|
|
52
|
+
const result = transform(
|
|
53
|
+
`const first = [1, 2, 3].filter(n => { const doubled = n * 2; return n > doubled; })[0];`,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert(
|
|
57
|
+
!result.modified,
|
|
58
|
+
"skip filter()[0] when predicate body has multiple statements",
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("predicate with member access on parameter", () => {
|
|
63
|
+
const result = transform(
|
|
64
|
+
`const first = ['ab', 'abc'].filter(x => x.length > 1)[0];`,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert(
|
|
68
|
+
result.modified,
|
|
69
|
+
"transform filter()[0] when predicate uses non-computed member access on parameter",
|
|
70
|
+
)
|
|
71
|
+
assert.match(result.code, /\.find\(x => x\.length > 1\)/)
|
|
72
|
+
assert.doesNotMatch(result.code, /filter/)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("array literal with arrow function", () => {
|
|
76
|
+
const result = transform(`const first = [1, 2, 3].filter(x => x > 0)[0];`)
|
|
77
|
+
|
|
78
|
+
assert(result.modified, "transform filter()[0] with arrow function")
|
|
79
|
+
assert.match(result.code, /\[1, 2, 3\]\.find\(x => x > 0\)/)
|
|
80
|
+
assert.doesNotMatch(result.code, /filter/)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("array literal with function expression", () => {
|
|
84
|
+
const result = transform(
|
|
85
|
+
`const first = [1, 2, 3].filter(function(n) { return n > 1; })[0];`,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert(result.modified, "transform filter()[0] with function expression")
|
|
89
|
+
assert.match(result.code, /\[1, 2, 3\]\.find\(/)
|
|
90
|
+
assert.doesNotMatch(result.code, /filter/)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("new Array() with arrow function", () => {
|
|
94
|
+
const result = transform(
|
|
95
|
+
`const first = new Array(1, 2, 3).filter(n => n > 1)[0];`,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert(result.modified, "transform filter()[0] on new Array()")
|
|
99
|
+
assert.match(result.code, /new Array\(1, 2, 3\)\.find\(n => n > 1\)/)
|
|
100
|
+
assert.doesNotMatch(result.code, /filter/)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
test("chained array method", () => {
|
|
104
|
+
const result = transform(
|
|
105
|
+
`const first = [1, 2, 3].map(n => n * 2).filter(n => n > 2)[0];`,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
assert(result.modified, "transform filter()[0] on chained array method")
|
|
109
|
+
assert.match(result.code, /\.find\(n => n > 2\)/)
|
|
110
|
+
assert.doesNotMatch(result.code, /filter/)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test("nested filter chain", () => {
|
|
114
|
+
const result = transform(
|
|
115
|
+
`const first = [1, 2, 3].filter(n => n > 0).filter(n => n < 3)[0];`,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
assert(result.modified, "transform filter()[0] on nested filter chain")
|
|
119
|
+
assert.match(result.code, /\.find\(n => n < 3\)/)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test("unknown receiver with arrow predicate - should not transform", () => {
|
|
123
|
+
const result = transform(`const first = arr.filter(n => n > 0)[0];`)
|
|
124
|
+
|
|
125
|
+
assert(
|
|
126
|
+
!result.modified,
|
|
127
|
+
"skip filter()[0] on unknown receiver even with inline predicate",
|
|
128
|
+
)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("non-filter method with [0] - should not transform", () => {
|
|
132
|
+
const result = transform(`const first = [1, 2, 3].sort(fn)[0];`)
|
|
133
|
+
|
|
134
|
+
assert(!result.modified, "skip [0] access on non-filter method call")
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test("function call result with [0] - should not transform", () => {
|
|
138
|
+
const result = transform(`const first = getItems()[0];`)
|
|
139
|
+
|
|
140
|
+
assert(!result.modified, "skip [0] access on plain function call result")
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test("unknown identifier - should not transform", () => {
|
|
144
|
+
const result = transform(`const first = arr.filter(fn)[0];`)
|
|
145
|
+
|
|
146
|
+
assert(!result.modified, "skip filter()[0] on unknown identifier")
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test("non-zero index - should not transform", () => {
|
|
150
|
+
const result = transform(`const second = [1, 2, 3].filter(fn)[1];`)
|
|
151
|
+
|
|
152
|
+
assert(!result.modified, "skip filter()[1] - not index 0")
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test("variable index - should not transform", () => {
|
|
156
|
+
const result = transform(`const item = [1, 2, 3].filter(fn)[n];`)
|
|
157
|
+
|
|
158
|
+
assert(!result.modified, "skip filter()[n] - computed variable index")
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test("no arguments to filter - should not transform", () => {
|
|
162
|
+
const result = transform(`const first = [1, 2, 3].filter()[0];`)
|
|
163
|
+
|
|
164
|
+
assert(!result.modified, "skip filter()[0] with no filter arguments")
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test("two arguments to filter - should not transform", () => {
|
|
168
|
+
const result = transform(`const first = [1, 2, 3].filter(fn, thisArg)[0];`)
|
|
169
|
+
|
|
170
|
+
assert(!result.modified, "skip filter()[0] with two filter arguments")
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import assert from "node:assert/strict"
|
|
2
|
+
import { describe, suite, test } from "node:test"
|
|
3
|
+
import { transform } from "../../src/index.js"
|
|
4
|
+
|
|
5
|
+
suite("widely-available", () => {
|
|
6
|
+
describe("objectKeysMapToValues", () => {
|
|
7
|
+
test("transform arrow function with expression body", () => {
|
|
8
|
+
const result = transform(`Object.keys(obj).map(key => obj[key])`)
|
|
9
|
+
|
|
10
|
+
assert(result.modified)
|
|
11
|
+
assert.match(result.code, /Object\.values\(obj\)/)
|
|
12
|
+
assert.doesNotMatch(result.code, /Object\.keys/)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test("transform arrow function with block body and return", () => {
|
|
16
|
+
const result = transform(`Object.keys(obj).map(key => { return obj[key]; })`)
|
|
17
|
+
|
|
18
|
+
assert(result.modified)
|
|
19
|
+
assert.match(result.code, /Object\.values\(obj\)/)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
test("transform regular function expression", () => {
|
|
23
|
+
const result = transform(
|
|
24
|
+
`Object.keys(obj).map(function(key) { return obj[key]; })`,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert(result.modified)
|
|
28
|
+
assert.match(result.code, /Object\.values\(obj\)/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test("transform when object is a member expression", () => {
|
|
32
|
+
const result = transform(
|
|
33
|
+
`Object.keys(config.options).map(key => config.options[key])`,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
assert(!result.modified)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test("skip when key not used as index", () => {
|
|
40
|
+
const result = transform(`Object.keys(obj).map(key => key.toUpperCase())`)
|
|
41
|
+
|
|
42
|
+
assert(!result.modified)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test("skip when accessing different object", () => {
|
|
46
|
+
const result = transform(`Object.keys(obj).map(key => other[key])`)
|
|
47
|
+
|
|
48
|
+
assert(!result.modified)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("skip when callback has multiple parameters", () => {
|
|
52
|
+
const result = transform(`Object.keys(obj).map((key, index) => obj[key])`)
|
|
53
|
+
|
|
54
|
+
assert(!result.modified)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test("skip when block body has multiple statements", () => {
|
|
58
|
+
const result = transform(
|
|
59
|
+
`Object.keys(obj).map(key => { const v = obj[key]; return v; })`,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert(!result.modified)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test("skip when map callback is a reference", () => {
|
|
66
|
+
const result = transform(`Object.keys(obj).map(getter)`)
|
|
67
|
+
|
|
68
|
+
assert(!result.modified)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test("skip when Object.keys called with multiple arguments", () => {
|
|
72
|
+
const result = transform(`Object.keys(obj, extra).map(key => obj[key])`)
|
|
73
|
+
|
|
74
|
+
assert(!result.modified)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test("skip when map called with thisArg", () => {
|
|
78
|
+
const result = transform(`Object.keys(obj).map(key => obj[key], ctx)`)
|
|
79
|
+
|
|
80
|
+
assert(!result.modified)
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test("skip when map is not called on Object.keys()", () => {
|
|
84
|
+
const result = transform(`[1, 2, 3].map(key => obj[key])`)
|
|
85
|
+
|
|
86
|
+
assert(!result.modified)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("skip when target object is a function call", () => {
|
|
90
|
+
const result = transform(`Object.keys(getObj()).map(key => getObj()[key])`)
|
|
91
|
+
|
|
92
|
+
assert(!result.modified)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test("skip when async arrow function callback", () => {
|
|
96
|
+
const result = transform(`Object.keys(obj).map(async key => obj[key])`)
|
|
97
|
+
|
|
98
|
+
assert(!result.modified)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test("skip when generator function callback", () => {
|
|
102
|
+
const result = transform(
|
|
103
|
+
`Object.keys(obj).map(function*(key) { return obj[key]; })`,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert(!result.modified)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
})
|