esupgrade 2025.20.0 → 2025.20.1
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.
|
@@ -5,12 +5,12 @@ name: SuperJoe
|
|
|
5
5
|
description: CodingJoe's digital clone following his coding guidelines and best practices.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
# SuperJoe
|
|
10
9
|
|
|
11
10
|
## Planning
|
|
12
11
|
|
|
13
12
|
You MUST ALWAYS follow the `naming-things` guidelines. Use the following command to access the guidelines:
|
|
13
|
+
|
|
14
14
|
```console
|
|
15
15
|
curl -sSL https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md | head -n 500
|
|
16
16
|
```
|
|
@@ -44,7 +44,6 @@ Avoid functions or other code inside functions.
|
|
|
44
44
|
Avoid if-statements in favor of switch/match-statements or polymorphism.
|
|
45
45
|
Do not assign names to objects which are returned in the next line.
|
|
46
46
|
|
|
47
|
-
|
|
48
47
|
## Python
|
|
49
48
|
|
|
50
49
|
Follow PEP 8 guidelines for code style.
|
package/.pre-commit-config.yaml
CHANGED
|
@@ -18,10 +18,10 @@ repos:
|
|
|
18
18
|
hooks:
|
|
19
19
|
- id: mdformat
|
|
20
20
|
additional_dependencies:
|
|
21
|
+
- mdformat-front-matters
|
|
21
22
|
- mdformat-footnote
|
|
22
23
|
- mdformat-gfm
|
|
23
24
|
- mdformat-gfm-alerts
|
|
24
|
-
exclude: '.github/agents/'
|
|
25
25
|
- repo: https://github.com/google/yamlfmt
|
|
26
26
|
rev: v0.21.0
|
|
27
27
|
hooks:
|
|
@@ -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.4.
|
|
35
|
+
rev: v10.4.1
|
|
36
36
|
hooks:
|
|
37
37
|
- id: eslint
|
|
38
38
|
args: ["--fix"]
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Keeping your JavaScript and TypeScript code up to date with full browser compati
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
|
15
15
|
esupgrade is safe and meant to be used automatically on your codebase.
|
|
16
|
-
We recommend integrating it into your development workflow using [pre-commit].
|
|
16
|
+
We recommend integrating it into your development workflow using [pre-commit] or [husky].
|
|
17
17
|
|
|
18
18
|
To try it out on a repository without writing changes, run:
|
|
19
19
|
|
|
@@ -40,6 +40,15 @@ repos:
|
|
|
40
40
|
pre-commit run esupgrade --all-files
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### Husky
|
|
44
|
+
|
|
45
|
+
Assuming Husky is already initialized and `.husky/pre-commit` already contains `set -e`, append:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; npx esupgrade -- \"\$@\"' sh" >> .husky/pre-commit
|
|
49
|
+
echo "git diff --cached --name-only --diff-filter=ACMR -z -- '*.js' '*.jsx' '*.ts' '*.tsx' '*.mjs' '*.cjs' | xargs -0 sh -c 'test \"\$#\" -eq 0 && exit 0; git add -- \"\$@\"' sh" >> .husky/pre-commit
|
|
50
|
+
```
|
|
51
|
+
|
|
43
52
|
### CLI
|
|
44
53
|
|
|
45
54
|
```bash
|
|
@@ -380,6 +389,7 @@ Transforms constructor functions (both function declarations and variable declar
|
|
|
380
389
|
|
|
381
390
|
- Function name starts with an uppercase letter
|
|
382
391
|
- At least one prototype method is defined
|
|
392
|
+
- Prototype methods are matched only to constructors declared in the current or an ancestor lexical scope (never sibling or child scopes)
|
|
383
393
|
- Prototype methods using `this` in arrow functions are skipped
|
|
384
394
|
- Prototype object literals with getters, setters, or computed properties are skipped
|
|
385
395
|
|
|
@@ -734,6 +744,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
|
|
|
734
744
|
[baseline]: https://web.dev/baseline/
|
|
735
745
|
[calver]: https://calver.org/
|
|
736
746
|
[django-upgrade]: https://github.com/adamchainz/django-upgrade
|
|
747
|
+
[husky]: https://typicode.github.io/husky/
|
|
737
748
|
[mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
738
749
|
[mdn-async-await]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
|
|
739
750
|
[mdn-at]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: esupgrade
|
|
3
|
+
description: Auto-update JavaScript and TypeScript syntax to new ECMAScript features based on browser support.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use the CLI with `npx` on files or directories:
|
|
7
|
+
|
|
8
|
+
```console
|
|
9
|
+
npx esupgrade [--baseline <newly-available|widely-available>] [--check] [--write] <files-or-directories>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- Use `--check` to preview changes and fail when updates are needed.
|
|
13
|
+
- Use `--write` to apply updates in place.
|
|
14
|
+
- Use `--baseline` to choose `newly-available` or `widely-available` (default).
|
package/package.json
CHANGED
|
@@ -1,33 +1,67 @@
|
|
|
1
1
|
import { default as j } from "jscodeshift"
|
|
2
2
|
import { NodeTest } from "../types.js"
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* @returns {boolean} True if code was modified
|
|
10
|
-
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
|
|
11
|
-
*/
|
|
12
|
-
export function constructorToClass(root) {
|
|
13
|
-
function findConstructors(root) {
|
|
14
|
-
const constructors = new Map()
|
|
15
|
-
|
|
16
|
-
root.find(j.FunctionDeclaration).forEach((path) => {
|
|
17
|
-
if (
|
|
18
|
-
!new NodeTest(path.node.id).isConstructorName() ||
|
|
19
|
-
!new NodeTest(path.node.body).hasSimpleConstructorBody()
|
|
20
|
-
) {
|
|
21
|
-
return
|
|
22
|
-
}
|
|
4
|
+
function getDeclarationScope(path) {
|
|
5
|
+
return j.FunctionDeclaration.check(path.node)
|
|
6
|
+
? (path.scope.parent ?? path.scope)
|
|
7
|
+
: path.scope
|
|
8
|
+
}
|
|
23
9
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
function addConstructor(constructorsByScope, constructorName, declarationPath) {
|
|
11
|
+
const scopeNode = getDeclarationScope(declarationPath).path.node
|
|
12
|
+
const constructorsInScope = constructorsByScope.get(scopeNode) ?? new Map()
|
|
13
|
+
|
|
14
|
+
if (constructorsInScope.has(constructorName)) {
|
|
15
|
+
constructorsInScope.set(constructorName, null)
|
|
16
|
+
constructorsByScope.set(scopeNode, constructorsInScope)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const constructorInfo = {
|
|
21
|
+
constructorName,
|
|
22
|
+
declaration: declarationPath,
|
|
23
|
+
prototypeMethods: [],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
constructorsInScope.set(constructorName, constructorInfo)
|
|
27
|
+
constructorsByScope.set(scopeNode, constructorsInScope)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getConstructor(constructorsByScope, path, constructorName) {
|
|
31
|
+
let activeScope = path.scope
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
while (activeScope) {
|
|
34
|
+
const constructorsInScope = constructorsByScope.get(activeScope.path.node)
|
|
35
|
+
const hasConstructorInScope = constructorsInScope?.has(constructorName)
|
|
36
|
+
|
|
37
|
+
if (hasConstructorInScope) {
|
|
38
|
+
return constructorsInScope.get(constructorName)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
activeScope = activeScope.parent
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findConstructors(root) {
|
|
48
|
+
const constructorsByScope = new Map()
|
|
49
|
+
|
|
50
|
+
root.find(j.FunctionDeclaration).forEach((path) => {
|
|
51
|
+
if (
|
|
52
|
+
!new NodeTest(path.node.id).isConstructorName() ||
|
|
53
|
+
!new NodeTest(path.node.body).hasSimpleConstructorBody()
|
|
54
|
+
) {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
addConstructor(constructorsByScope, path.node.id.name, path)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
root
|
|
62
|
+
.find(j.VariableDeclaration)
|
|
63
|
+
.filter((path) => path.node.declarations.length === 1)
|
|
64
|
+
.forEach((path) => {
|
|
31
65
|
path.node.declarations.forEach((declarator) => {
|
|
32
66
|
if (
|
|
33
67
|
!new NodeTest(declarator.id).isConstructorName() ||
|
|
@@ -37,153 +71,166 @@ export function constructorToClass(root) {
|
|
|
37
71
|
return
|
|
38
72
|
}
|
|
39
73
|
|
|
40
|
-
|
|
41
|
-
declaration: path,
|
|
42
|
-
prototypeMethods: [],
|
|
43
|
-
})
|
|
74
|
+
addConstructor(constructorsByScope, declarator.id.name, path)
|
|
44
75
|
})
|
|
45
76
|
})
|
|
46
77
|
|
|
47
|
-
|
|
48
|
-
|
|
78
|
+
return constructorsByScope
|
|
79
|
+
}
|
|
49
80
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Find and associate prototype methods with constructors.
|
|
83
|
+
*
|
|
84
|
+
* @param {import("jscodeshift").Collection} root - The root AST collection.
|
|
85
|
+
* @param {Map<
|
|
86
|
+
* object,
|
|
87
|
+
* Map<
|
|
88
|
+
* string,
|
|
89
|
+
* {
|
|
90
|
+
* constructorName: string
|
|
91
|
+
* declaration: import("ast-types").NodePath
|
|
92
|
+
* prototypeMethods: any[]
|
|
93
|
+
* }
|
|
94
|
+
* >
|
|
95
|
+
* >} constructorsByScope
|
|
96
|
+
* - Map of constructors grouped by lexical scope.
|
|
97
|
+
*/
|
|
98
|
+
function findPrototypeMethods(root, constructorsByScope) {
|
|
99
|
+
// Pattern 1: ConstructorName.prototype.methodName = ...
|
|
100
|
+
root.find(j.ExpressionStatement).forEach((path) => {
|
|
101
|
+
const { node } = path
|
|
102
|
+
|
|
103
|
+
if (!j.AssignmentExpression.check(node.expression)) {
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const assignment = node.expression
|
|
108
|
+
const left = assignment.left
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
!j.MemberExpression.check(left) ||
|
|
112
|
+
!j.MemberExpression.check(left.object) ||
|
|
113
|
+
!j.Identifier.check(left.object.object) ||
|
|
114
|
+
!j.Identifier.check(left.object.property) ||
|
|
115
|
+
left.object.property.name !== "prototype" ||
|
|
116
|
+
!j.Identifier.check(left.property)
|
|
117
|
+
) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const constructorName = left.object.object.name
|
|
122
|
+
const constructorInfo = getConstructor(constructorsByScope, path, constructorName)
|
|
123
|
+
|
|
124
|
+
if (!constructorInfo) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const methodName = left.property.name
|
|
129
|
+
const methodValue = assignment.right
|
|
130
|
+
|
|
131
|
+
if (!new NodeTest(methodValue).canBeClassMethod()) {
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
constructorInfo.prototypeMethods.push({
|
|
136
|
+
path,
|
|
137
|
+
methodName,
|
|
138
|
+
methodValue,
|
|
139
|
+
})
|
|
140
|
+
})
|
|
68
141
|
|
|
69
|
-
|
|
70
|
-
|
|
142
|
+
// Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
|
|
143
|
+
root.find(j.ExpressionStatement).forEach((path) => {
|
|
144
|
+
const { node } = path
|
|
71
145
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
!j.Identifier.check(left.object.object) ||
|
|
76
|
-
!j.Identifier.check(left.object.property) ||
|
|
77
|
-
left.object.property.name !== "prototype" ||
|
|
78
|
-
!j.Identifier.check(left.property)
|
|
79
|
-
) {
|
|
80
|
-
return false
|
|
81
|
-
}
|
|
146
|
+
if (!j.AssignmentExpression.check(node.expression)) {
|
|
147
|
+
return
|
|
148
|
+
}
|
|
82
149
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
.forEach((path) => {
|
|
87
|
-
const assignment = path.node.expression
|
|
88
|
-
const left = assignment.left
|
|
89
|
-
const constructorName = left.object.object.name
|
|
90
|
-
const methodName = left.property.name
|
|
91
|
-
const methodValue = assignment.right
|
|
92
|
-
|
|
93
|
-
if (!new NodeTest(methodValue).canBeClassMethod()) {
|
|
94
|
-
return
|
|
95
|
-
}
|
|
150
|
+
const assignment = node.expression
|
|
151
|
+
const left = assignment.left
|
|
96
152
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
153
|
+
if (
|
|
154
|
+
!j.MemberExpression.check(left) ||
|
|
155
|
+
!j.Identifier.check(left.object) ||
|
|
156
|
+
!j.Identifier.check(left.property) ||
|
|
157
|
+
left.property.name !== "prototype"
|
|
158
|
+
) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
103
161
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.find(j.ExpressionStatement)
|
|
107
|
-
.filter(({ node }) => {
|
|
108
|
-
if (!j.AssignmentExpression.check(node.expression)) {
|
|
109
|
-
return false
|
|
110
|
-
}
|
|
162
|
+
const constructorName = left.object.name
|
|
163
|
+
const constructorInfo = getConstructor(constructorsByScope, path, constructorName)
|
|
111
164
|
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
if (!constructorInfo) {
|
|
166
|
+
return
|
|
167
|
+
}
|
|
114
168
|
|
|
115
|
-
|
|
116
|
-
!j.MemberExpression.check(left) ||
|
|
117
|
-
!j.Identifier.check(left.object) ||
|
|
118
|
-
!j.Identifier.check(left.property) ||
|
|
119
|
-
left.property.name !== "prototype"
|
|
120
|
-
) {
|
|
121
|
-
return false
|
|
122
|
-
}
|
|
169
|
+
const methodValue = assignment.right
|
|
123
170
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
.forEach((path) => {
|
|
128
|
-
const assignment = path.node.expression
|
|
129
|
-
const left = assignment.left
|
|
130
|
-
const constructorName = left.object.name
|
|
131
|
-
const methodValue = assignment.right
|
|
171
|
+
if (!j.ObjectExpression.check(methodValue)) {
|
|
172
|
+
return
|
|
173
|
+
}
|
|
132
174
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
methodValue.properties.forEach((prop) => {
|
|
176
|
+
if (!j.Property.check(prop) && !j.ObjectProperty.check(prop)) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (prop.computed) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let methodName
|
|
185
|
+
if (j.Identifier.check(prop.key)) {
|
|
186
|
+
methodName = prop.key.name
|
|
187
|
+
} else {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!new NodeTest(prop.value).canBeClassMethod()) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
136
194
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (prop.computed) {
|
|
143
|
-
return
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
let methodName
|
|
147
|
-
if (j.Identifier.check(prop.key)) {
|
|
148
|
-
methodName = prop.key.name
|
|
149
|
-
} else {
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!new NodeTest(prop.value).canBeClassMethod()) {
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
constructors.get(constructorName).prototypeMethods.push({
|
|
158
|
-
path,
|
|
159
|
-
methodName,
|
|
160
|
-
methodValue: prop.value,
|
|
161
|
-
isObjectLiteral: true,
|
|
162
|
-
})
|
|
163
|
-
})
|
|
195
|
+
constructorInfo.prototypeMethods.push({
|
|
196
|
+
path,
|
|
197
|
+
methodName,
|
|
198
|
+
methodValue: prop.value,
|
|
199
|
+
isObjectLiteral: true,
|
|
164
200
|
})
|
|
165
|
-
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Transform constructors and their prototype methods to class syntax.
|
|
207
|
+
*
|
|
208
|
+
* @param {import("jscodeshift").Collection} root - The root AST collection.
|
|
209
|
+
* @param {Map<
|
|
210
|
+
* object,
|
|
211
|
+
* Map<
|
|
212
|
+
* string,
|
|
213
|
+
* {
|
|
214
|
+
* constructorName: string
|
|
215
|
+
* declaration: import("ast-types").NodePath
|
|
216
|
+
* prototypeMethods: any[]
|
|
217
|
+
* }
|
|
218
|
+
* >
|
|
219
|
+
* >} constructorsByScope
|
|
220
|
+
* - Map of constructors grouped by lexical scope.
|
|
221
|
+
*
|
|
222
|
+
* @returns {boolean} True if code was modified.
|
|
223
|
+
*/
|
|
224
|
+
function transformConstructorsToClasses(root, constructorsByScope) {
|
|
225
|
+
let modified = false
|
|
166
226
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
* @param {import("jscodeshift").Collection} root - The root AST collection.
|
|
171
|
-
* @param {Map<
|
|
172
|
-
* string,
|
|
173
|
-
* { declaration: import("ast-types").NodePath; prototypeMethods: any[] }
|
|
174
|
-
* >} constructors
|
|
175
|
-
* - Map of constructors.
|
|
176
|
-
*
|
|
177
|
-
* @returns {boolean} True if code was modified.
|
|
178
|
-
*/
|
|
179
|
-
function transformConstructorsToClasses(root, constructors) {
|
|
180
|
-
let modified = false
|
|
181
|
-
|
|
182
|
-
constructors.forEach((info, constructorName) => {
|
|
183
|
-
if (info.prototypeMethods.length === 0) {
|
|
227
|
+
constructorsByScope.forEach((constructors) => {
|
|
228
|
+
constructors.forEach((info) => {
|
|
229
|
+
if (!info || info.prototypeMethods.length === 0) {
|
|
184
230
|
return
|
|
185
231
|
}
|
|
186
232
|
|
|
233
|
+
const constructorName = info.constructorName
|
|
187
234
|
const declarationPath = info.declaration
|
|
188
235
|
const declarationNode = declarationPath.node
|
|
189
236
|
|
|
@@ -197,20 +244,29 @@ export function constructorToClass(root) {
|
|
|
197
244
|
constructorNode = declarator.init
|
|
198
245
|
}
|
|
199
246
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
247
|
+
const hasNodeOrBodyComments =
|
|
248
|
+
(constructorNode.comments && constructorNode.comments.length > 0) ||
|
|
249
|
+
(constructorNode.body.comments && constructorNode.body.comments.length > 0)
|
|
250
|
+
|
|
251
|
+
const classBody =
|
|
252
|
+
constructorNode.params.length > 0 ||
|
|
253
|
+
constructorNode.body.body.length > 0 ||
|
|
254
|
+
hasNodeOrBodyComments
|
|
255
|
+
? [
|
|
256
|
+
j.methodDefinition(
|
|
257
|
+
"constructor",
|
|
258
|
+
j.identifier("constructor"),
|
|
259
|
+
j.functionExpression(
|
|
260
|
+
null,
|
|
261
|
+
constructorNode.params,
|
|
262
|
+
constructorNode.body,
|
|
263
|
+
constructorNode.generator,
|
|
264
|
+
constructorNode.async,
|
|
265
|
+
),
|
|
266
|
+
false,
|
|
267
|
+
),
|
|
268
|
+
]
|
|
269
|
+
: []
|
|
214
270
|
|
|
215
271
|
info.prototypeMethods.forEach(({ methodName, methodValue }) => {
|
|
216
272
|
const functionExpr = j.functionExpression(
|
|
@@ -254,12 +310,22 @@ export function constructorToClass(root) {
|
|
|
254
310
|
|
|
255
311
|
modified = true
|
|
256
312
|
})
|
|
313
|
+
})
|
|
257
314
|
|
|
258
|
-
|
|
259
|
-
|
|
315
|
+
return modified
|
|
316
|
+
}
|
|
260
317
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
318
|
+
/**
|
|
319
|
+
* Transform old-school constructor functions with prototype methods to ES6 class
|
|
320
|
+
* syntax.
|
|
321
|
+
*
|
|
322
|
+
* @param {import("jscodeshift").Collection} root - The root AST collection
|
|
323
|
+
* @returns {boolean} True if code was modified
|
|
324
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
|
|
325
|
+
*/
|
|
326
|
+
export function constructorToClass(root) {
|
|
327
|
+
const constructorsByScope = findConstructors(root)
|
|
328
|
+
findPrototypeMethods(root, constructorsByScope)
|
|
329
|
+
return transformConstructorsToClasses(root, constructorsByScope)
|
|
264
330
|
}
|
|
265
331
|
constructorToClass.baselineDate = new Date(Date.UTC(2016, 2, 8))
|
|
@@ -710,5 +710,336 @@ return this.value;
|
|
|
710
710
|
assert(result.modified, "transform function declaration with empty body")
|
|
711
711
|
assert.match(result.code, /class Empty/)
|
|
712
712
|
})
|
|
713
|
+
|
|
714
|
+
test("omit empty constructor from generated class", () => {
|
|
715
|
+
const result = transform(`
|
|
716
|
+
function Empty() {}
|
|
717
|
+
|
|
718
|
+
Empty.prototype.run = function() {
|
|
719
|
+
return this.value;
|
|
720
|
+
};
|
|
721
|
+
`)
|
|
722
|
+
|
|
723
|
+
assert(result.modified, "transform function declaration with empty body")
|
|
724
|
+
assert.match(result.code, /class Empty/)
|
|
725
|
+
assert.doesNotMatch(result.code, /constructor\s*\(/)
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
test("keep constructor when empty body contains comments", () => {
|
|
729
|
+
const result = transform(`
|
|
730
|
+
function Empty() {
|
|
731
|
+
/* important */
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
Empty.prototype.run = function() {
|
|
735
|
+
return this.value;
|
|
736
|
+
};
|
|
737
|
+
`)
|
|
738
|
+
|
|
739
|
+
assert(result.modified, "transform function declaration with comment-only body")
|
|
740
|
+
assert.match(result.code, /class Empty/)
|
|
741
|
+
assert.match(result.code, /constructor\s*\(/)
|
|
742
|
+
assert.match(result.code, /important/)
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
test("keep function declaration prototype methods in matching sibling scope", () => {
|
|
746
|
+
const result = transform(`
|
|
747
|
+
function firstSuite() {
|
|
748
|
+
function BaseClass() {}
|
|
749
|
+
|
|
750
|
+
BaseClass.prototype.first = function() {
|
|
751
|
+
return 'first';
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
function secondSuite() {
|
|
756
|
+
function BaseClass() {}
|
|
757
|
+
|
|
758
|
+
BaseClass.prototype.second = function() {
|
|
759
|
+
return 'second';
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
`)
|
|
763
|
+
|
|
764
|
+
assert(result.modified, "transform both constructors")
|
|
765
|
+
assert.equal(result.code.split("first() {").length - 1, 1)
|
|
766
|
+
assert.equal(result.code.split("second() {").length - 1, 1)
|
|
767
|
+
const [firstSuiteCode, secondSuiteCode] = result.code.split(
|
|
768
|
+
"function secondSuite()",
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
assert.match(firstSuiteCode, /first\(\) \{/, "keep first method in first scope")
|
|
772
|
+
assert.doesNotMatch(
|
|
773
|
+
firstSuiteCode,
|
|
774
|
+
/second\(\) \{/,
|
|
775
|
+
"avoid leaking second method into first scope",
|
|
776
|
+
)
|
|
777
|
+
assert.match(
|
|
778
|
+
secondSuiteCode,
|
|
779
|
+
/second\(\) \{/,
|
|
780
|
+
"keep second method in second scope",
|
|
781
|
+
)
|
|
782
|
+
assert.doesNotMatch(
|
|
783
|
+
secondSuiteCode,
|
|
784
|
+
/first\(\) \{/,
|
|
785
|
+
"avoid leaking first method into second scope",
|
|
786
|
+
)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
test("keep variable declaration prototype methods in matching sibling scope", () => {
|
|
790
|
+
const result = transform(`
|
|
791
|
+
function firstSuite() {
|
|
792
|
+
var BaseClass = function() {};
|
|
793
|
+
|
|
794
|
+
BaseClass.prototype.first = function() {
|
|
795
|
+
return 'first';
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function secondSuite() {
|
|
800
|
+
var BaseClass = function() {};
|
|
801
|
+
|
|
802
|
+
BaseClass.prototype.second = function() {
|
|
803
|
+
return 'second';
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
`)
|
|
807
|
+
|
|
808
|
+
assert(result.modified, "transform both constructors")
|
|
809
|
+
assert.equal(result.code.split("first() {").length - 1, 1)
|
|
810
|
+
assert.equal(result.code.split("second() {").length - 1, 1)
|
|
811
|
+
const [firstSuiteCode, secondSuiteCode] = result.code.split(
|
|
812
|
+
"function secondSuite()",
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
assert.match(firstSuiteCode, /first\(\) \{/, "keep first method in first scope")
|
|
816
|
+
assert.doesNotMatch(
|
|
817
|
+
firstSuiteCode,
|
|
818
|
+
/second\(\) \{/,
|
|
819
|
+
"avoid leaking second method into first scope",
|
|
820
|
+
)
|
|
821
|
+
assert.match(
|
|
822
|
+
secondSuiteCode,
|
|
823
|
+
/second\(\) \{/,
|
|
824
|
+
"keep second method in second scope",
|
|
825
|
+
)
|
|
826
|
+
assert.doesNotMatch(
|
|
827
|
+
secondSuiteCode,
|
|
828
|
+
/first\(\) \{/,
|
|
829
|
+
"avoid leaking first method into second scope",
|
|
830
|
+
)
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
test("keep prototype object assignments in matching sibling scope", () => {
|
|
834
|
+
const result = transform(`
|
|
835
|
+
QUnit.test('one', function(assert) {
|
|
836
|
+
function BaseClass() {}
|
|
837
|
+
|
|
838
|
+
BaseClass.prototype = {
|
|
839
|
+
hello: function() {
|
|
840
|
+
return 'A';
|
|
841
|
+
}
|
|
842
|
+
};
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
QUnit.test('two', function(assert) {
|
|
846
|
+
function BaseClass() {}
|
|
847
|
+
|
|
848
|
+
BaseClass.prototype = {
|
|
849
|
+
goodbye: function() {
|
|
850
|
+
return 'B';
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
`)
|
|
855
|
+
|
|
856
|
+
assert(result.modified, "transform both QUnit constructors")
|
|
857
|
+
assert.equal(result.code.split("hello() {").length - 1, 1)
|
|
858
|
+
assert.equal(result.code.split("goodbye() {").length - 1, 1)
|
|
859
|
+
const [firstTestCode, secondTestCode] = result.code.split("QUnit.test('two'")
|
|
860
|
+
|
|
861
|
+
assert.match(firstTestCode, /hello\(\) \{/, "keep hello in first test scope")
|
|
862
|
+
assert.doesNotMatch(
|
|
863
|
+
firstTestCode,
|
|
864
|
+
/goodbye\(\) \{/,
|
|
865
|
+
"avoid leaking goodbye into first test scope",
|
|
866
|
+
)
|
|
867
|
+
assert.match(
|
|
868
|
+
secondTestCode,
|
|
869
|
+
/goodbye\(\) \{/,
|
|
870
|
+
"keep goodbye in second test scope",
|
|
871
|
+
)
|
|
872
|
+
assert.doesNotMatch(
|
|
873
|
+
secondTestCode,
|
|
874
|
+
/hello\(\) \{/,
|
|
875
|
+
"avoid leaking hello into second test scope",
|
|
876
|
+
)
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
test("match prototype methods to the nearest constructor scope", () => {
|
|
880
|
+
const result = transform(`
|
|
881
|
+
function outerSuite() {
|
|
882
|
+
function BaseClass() {}
|
|
883
|
+
|
|
884
|
+
function addOuterMethod() {
|
|
885
|
+
BaseClass.prototype.outer = function() {
|
|
886
|
+
return 'outer';
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
addOuterMethod();
|
|
891
|
+
|
|
892
|
+
function innerSuite() {
|
|
893
|
+
function BaseClass() {}
|
|
894
|
+
|
|
895
|
+
BaseClass.prototype.inner = function() {
|
|
896
|
+
return 'inner';
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
`)
|
|
901
|
+
|
|
902
|
+
assert(result.modified, "transform constructors across nested scopes")
|
|
903
|
+
assert.equal(result.code.split("outer() {").length - 1, 1)
|
|
904
|
+
assert.equal(result.code.split("inner() {").length - 1, 1)
|
|
905
|
+
const [outerSuiteCode, innerSuiteCode] = result.code.split(
|
|
906
|
+
"function innerSuite()",
|
|
907
|
+
)
|
|
908
|
+
|
|
909
|
+
assert.match(outerSuiteCode, /outer\(\) \{/, "keep outer method on outer class")
|
|
910
|
+
assert.doesNotMatch(
|
|
911
|
+
outerSuiteCode,
|
|
912
|
+
/inner\(\) \{/,
|
|
913
|
+
"avoid leaking inner method into outer scope",
|
|
914
|
+
)
|
|
915
|
+
assert.match(innerSuiteCode, /inner\(\) \{/, "keep inner method on inner class")
|
|
916
|
+
assert.doesNotMatch(
|
|
917
|
+
innerSuiteCode,
|
|
918
|
+
/outer\(\) \{/,
|
|
919
|
+
"avoid leaking outer method into inner scope",
|
|
920
|
+
)
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
test("skip duplicate constructor declarations in the same scope", () => {
|
|
924
|
+
const result = transform(`
|
|
925
|
+
function wrapper() {
|
|
926
|
+
function BaseClass() {}
|
|
927
|
+
function BaseClass() {}
|
|
928
|
+
|
|
929
|
+
BaseClass.prototype.run = function() {
|
|
930
|
+
return 'run';
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
`)
|
|
934
|
+
|
|
935
|
+
assert.match(result.code, /function BaseClass\(\) \{\}/)
|
|
936
|
+
assert.doesNotMatch(result.code, /class BaseClass/)
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
test("do not leak duplicate-scope prototype methods to parent constructors", () => {
|
|
940
|
+
const result = transform(`
|
|
941
|
+
function BaseClass() {}
|
|
942
|
+
|
|
943
|
+
function wrapper() {
|
|
944
|
+
function BaseClass() {}
|
|
945
|
+
function BaseClass() {}
|
|
946
|
+
|
|
947
|
+
BaseClass.prototype.inner = function() {
|
|
948
|
+
return 'inner';
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
BaseClass.prototype.outer = function() {
|
|
953
|
+
return 'outer';
|
|
954
|
+
};
|
|
955
|
+
`)
|
|
956
|
+
|
|
957
|
+
assert(result.modified, "transform unambiguous outer constructor")
|
|
958
|
+
const [wrapperCode, outerCode] = result.code.split("function wrapper()")
|
|
959
|
+
assert.match(wrapperCode, /class BaseClass/)
|
|
960
|
+
assert.match(wrapperCode, /outer\(\) \{/, "keep outer method on outer class")
|
|
961
|
+
assert.match(
|
|
962
|
+
outerCode,
|
|
963
|
+
/BaseClass\.prototype\.inner/,
|
|
964
|
+
"keep ambiguous inner scope assignment untouched",
|
|
965
|
+
)
|
|
966
|
+
assert.doesNotMatch(
|
|
967
|
+
wrapperCode,
|
|
968
|
+
/inner\(\) \{/,
|
|
969
|
+
"avoid leaking duplicate-scope method into parent constructor",
|
|
970
|
+
)
|
|
971
|
+
})
|
|
972
|
+
|
|
973
|
+
test("do not leak duplicate-scope prototype object assignment to parent constructors", () => {
|
|
974
|
+
const result = transform(`
|
|
975
|
+
function BaseClass() {}
|
|
976
|
+
|
|
977
|
+
function wrapper() {
|
|
978
|
+
function BaseClass() {}
|
|
979
|
+
function BaseClass() {}
|
|
980
|
+
|
|
981
|
+
BaseClass.prototype = {
|
|
982
|
+
inner: function() {
|
|
983
|
+
return 'inner';
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
BaseClass.prototype = {
|
|
989
|
+
outer: function() {
|
|
990
|
+
return 'outer';
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
`)
|
|
994
|
+
|
|
995
|
+
assert(result.modified, "transform unambiguous outer constructor")
|
|
996
|
+
const [wrapperCode, outerCode] = result.code.split("function wrapper()")
|
|
997
|
+
assert.match(wrapperCode, /class BaseClass/)
|
|
998
|
+
assert.match(wrapperCode, /outer\(\) \{/, "keep outer method on outer class")
|
|
999
|
+
assert.match(
|
|
1000
|
+
outerCode,
|
|
1001
|
+
/BaseClass\.prototype = \{/,
|
|
1002
|
+
"keep ambiguous inner scope assignment untouched",
|
|
1003
|
+
)
|
|
1004
|
+
assert.doesNotMatch(
|
|
1005
|
+
wrapperCode,
|
|
1006
|
+
/inner\(\) \{/,
|
|
1007
|
+
"avoid leaking duplicate-scope object assignment into parent constructor",
|
|
1008
|
+
)
|
|
1009
|
+
})
|
|
1010
|
+
|
|
1011
|
+
test("transform multi-declarator constructors after safe splitting", () => {
|
|
1012
|
+
const result = transform(`
|
|
1013
|
+
var First = function() {}, Second = function() {};
|
|
1014
|
+
|
|
1015
|
+
First.prototype.run = function() {
|
|
1016
|
+
return 'first';
|
|
1017
|
+
};
|
|
1018
|
+
Second.prototype.stop = function() {
|
|
1019
|
+
return 'second';
|
|
1020
|
+
};
|
|
1021
|
+
`)
|
|
1022
|
+
|
|
1023
|
+
assert(result.modified, "transform after safe split")
|
|
1024
|
+
assert.match(result.code, /class First/)
|
|
1025
|
+
assert.match(result.code, /class Second/)
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
test("do not match constructors declared in child lexical scopes", () => {
|
|
1029
|
+
const result = transform(`
|
|
1030
|
+
function wrapper() {
|
|
1031
|
+
BaseClass.prototype.run = function() {
|
|
1032
|
+
return 'run';
|
|
1033
|
+
};
|
|
1034
|
+
|
|
1035
|
+
function setup() {
|
|
1036
|
+
function BaseClass() {}
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
`)
|
|
1040
|
+
|
|
1041
|
+
assert.match(result.code, /function BaseClass\(\) \{\}/)
|
|
1042
|
+
assert.doesNotMatch(result.code, /class BaseClass/)
|
|
1043
|
+
})
|
|
713
1044
|
})
|
|
714
1045
|
})
|