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.
@@ -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.0
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,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.20.0",
3
+ "version": "2025.20.1",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,33 +1,67 @@
1
1
  import { default as j } from "jscodeshift"
2
2
  import { NodeTest } from "../types.js"
3
3
 
4
- /**
5
- * Transform old-school constructor functions with prototype methods to ES6 class
6
- * syntax.
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/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
- constructors.set(path.node.id.name, {
25
- declaration: path,
26
- prototypeMethods: [],
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
- root.find(j.VariableDeclaration).forEach((path) => {
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
- constructors.set(declarator.id.name, {
41
- declaration: path,
42
- prototypeMethods: [],
43
- })
74
+ addConstructor(constructorsByScope, declarator.id.name, path)
44
75
  })
45
76
  })
46
77
 
47
- return constructors
48
- }
78
+ return constructorsByScope
79
+ }
49
80
 
50
- /**
51
- * Find and associate prototype methods with constructors.
52
- *
53
- * @param {import("jscodeshift").Collection} root - The root AST collection.
54
- * @param {Map<
55
- * string,
56
- * { declaration: import("ast-types").NodePath; prototypeMethods: any[] }
57
- * >} constructors
58
- * - Map of constructors.
59
- */
60
- function findPrototypeMethods(root, constructors) {
61
- // Pattern 1: ConstructorName.prototype.methodName = ...
62
- root
63
- .find(j.ExpressionStatement)
64
- .filter(({ node }) => {
65
- if (!j.AssignmentExpression.check(node.expression)) {
66
- return false
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
- const assignment = node.expression
70
- const left = assignment.left
142
+ // Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
143
+ root.find(j.ExpressionStatement).forEach((path) => {
144
+ const { node } = path
71
145
 
72
- if (
73
- !j.MemberExpression.check(left) ||
74
- !j.MemberExpression.check(left.object) ||
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
- const constructorName = left.object.object.name
84
- return constructors.has(constructorName)
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
- constructors.get(constructorName).prototypeMethods.push({
98
- path,
99
- methodName,
100
- methodValue,
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
- // Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
105
- root
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
- const assignment = node.expression
113
- const left = assignment.left
165
+ if (!constructorInfo) {
166
+ return
167
+ }
114
168
 
115
- if (
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
- const constructorName = left.object.name
125
- return constructors.has(constructorName)
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
- if (!j.ObjectExpression.check(methodValue)) {
134
- return
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
- methodValue.properties.forEach((prop) => {
138
- if (!j.Property.check(prop) && !j.ObjectProperty.check(prop)) {
139
- return
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
- * Transform constructors and their prototype methods to class syntax.
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 classBody = [
201
- j.methodDefinition(
202
- "constructor",
203
- j.identifier("constructor"),
204
- j.functionExpression(
205
- null,
206
- constructorNode.params,
207
- constructorNode.body,
208
- constructorNode.generator,
209
- constructorNode.async,
210
- ),
211
- false,
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
- return modified
259
- }
315
+ return modified
316
+ }
260
317
 
261
- const constructors = findConstructors(root)
262
- findPrototypeMethods(root, constructors)
263
- return transformConstructorsToClasses(root, constructors)
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
  })