esupgrade 2025.15.0 → 2025.16.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.
@@ -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 | cat
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.
@@ -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@v5
31
+ - uses: codecov/codecov-action@v6
32
32
  with:
33
33
  token: ${{ secrets.CODECOV_TOKEN }}
34
34
  flags: javascript
@@ -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.0.0-rc.1
35
+ rev: v10.3.0
35
36
  hooks:
36
37
  - id: eslint
37
38
  args: ["--fix"]
package/README.md CHANGED
@@ -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
@@ -785,6 +796,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
785
796
  [mdn-default-parameters]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Default_parameters
786
797
  [mdn-endswith]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith
787
798
  [mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
799
+ [mdn-find]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
788
800
  [mdn-for-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of
789
801
  [mdn-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions
790
802
  [mdn-globalthis]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.15.0",
3
+ "version": "2025.16.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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
+ }
@@ -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"
@@ -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
+ })