esupgrade 2025.3.4 → 2025.4.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/copilot-instructions.md +18 -0
- package/README.md +42 -0
- package/package.json +1 -1
- package/src/widelyAvailable.js +303 -0
- package/tests/widelyAvailable.test.js +575 -0
- package/AGENTS.md +0 -5
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
When writing code, you MUST ALWAYS follow the [naming-things](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md) guidelines.
|
|
2
|
+
|
|
3
|
+
All code must be fully tested with a 100% coverage. Unreachable code must be removed.
|
|
4
|
+
|
|
5
|
+
All transformers must be documented in the README.md.
|
|
6
|
+
|
|
7
|
+
Use class syntax for all object-oriented code.
|
|
8
|
+
Use named functions instead of anonymous functions whenever possible.
|
|
9
|
+
Use `#` for private methods.
|
|
10
|
+
|
|
11
|
+
Avoid overly complex functions. Break them into smaller functions if necessary.
|
|
12
|
+
|
|
13
|
+
Write docstrings with jsdoc type annotations for all functions, classes, and methods.
|
|
14
|
+
Docstrings should be written in present tense imperative mood.
|
|
15
|
+
Docstrings must describe the external behavior of the function, class, or method.
|
|
16
|
+
Docstrings should avoid redundant phrases like "This function" or "This method".
|
|
17
|
+
Class docstrings must not repeat the class name or start with a verb since they don't do anything themselves.
|
|
18
|
+
Avoid code comments unless they describe behavior of 3rd party code or complex algorithms.
|
package/README.md
CHANGED
|
@@ -212,6 +212,47 @@ Supports:
|
|
|
212
212
|
> [!NOTE]
|
|
213
213
|
> Functions using `this`, `arguments`, or `super` are not converted to preserve semantics.
|
|
214
214
|
|
|
215
|
+
#### Constructor functions → [Classes][mdn-classes]
|
|
216
|
+
|
|
217
|
+
```diff
|
|
218
|
+
-function Person(name, age) {
|
|
219
|
+
- this.name = name;
|
|
220
|
+
- this.age = age;
|
|
221
|
+
-}
|
|
222
|
+
-
|
|
223
|
+
-Person.prototype.greet = function() {
|
|
224
|
+
- return 'Hello, I am ' + this.name;
|
|
225
|
+
-};
|
|
226
|
+
-
|
|
227
|
+
-Person.prototype.getAge = function() {
|
|
228
|
+
- return this.age;
|
|
229
|
+
-};
|
|
230
|
+
+class Person {
|
|
231
|
+
+ constructor(name, age) {
|
|
232
|
+
+ this.name = name;
|
|
233
|
+
+ this.age = age;
|
|
234
|
+
+ }
|
|
235
|
+
+
|
|
236
|
+
+ greet() {
|
|
237
|
+
+ return 'Hello, I am ' + this.name;
|
|
238
|
+
+ }
|
|
239
|
+
+
|
|
240
|
+
+ getAge() {
|
|
241
|
+
+ return this.age;
|
|
242
|
+
+ }
|
|
243
|
+
+}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> [!NOTE]
|
|
247
|
+
> Transforms constructor functions (both function declarations and variable declarations) that meet these criteria:
|
|
248
|
+
>
|
|
249
|
+
> - Function name starts with an uppercase letter
|
|
250
|
+
> - Constructor bodies are limited to simple statements (variable declarations and expression statements)
|
|
251
|
+
> - No control flow statements (`if`, `for`, `while`, `return`, `throw`, etc.) in constructor body
|
|
252
|
+
> - At least one prototype method is defined
|
|
253
|
+
> - Prototype methods must be function expressions (not arrow functions)
|
|
254
|
+
> - Prototype object literals with getters, setters, or computed properties are skipped
|
|
255
|
+
|
|
215
256
|
<picture>
|
|
216
257
|
<source media="(prefers-color-scheme: dark)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word-dark.svg">
|
|
217
258
|
<source media="(prefers-color-scheme: light)" srcset="https://web-platform-dx.github.io/web-features/assets/img/baseline-newly-word.svg">
|
|
@@ -258,6 +299,7 @@ Furthermore, esupgrade supports JavaScript, TypeScript, and more, while lebab is
|
|
|
258
299
|
[calver]: https://calver.org/
|
|
259
300
|
[django-upgrade]: https://github.com/adamchainz/django-upgrade
|
|
260
301
|
[mdn-arrow-functions]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
|
|
302
|
+
[mdn-classes]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
|
|
261
303
|
[mdn-const]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const
|
|
262
304
|
[mdn-exponentiation]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Exponentiation
|
|
263
305
|
[mdn-for-in]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...in
|
package/package.json
CHANGED
package/src/widelyAvailable.js
CHANGED
|
@@ -1363,3 +1363,306 @@ export function arrayConcatToSpread(j, root) {
|
|
|
1363
1363
|
|
|
1364
1364
|
return { modified, changes }
|
|
1365
1365
|
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* Transform old-school constructor functions with prototype methods to ES6 class syntax
|
|
1369
|
+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes
|
|
1370
|
+
*/
|
|
1371
|
+
export function constructorToClass(j, root) {
|
|
1372
|
+
/**
|
|
1373
|
+
* Check if a function name follows constructor naming convention.
|
|
1374
|
+
* @param {string} name - The function name to check.
|
|
1375
|
+
* @returns {boolean} True if the name starts with an uppercase letter.
|
|
1376
|
+
*/
|
|
1377
|
+
function isConstructorName(name) {
|
|
1378
|
+
return name && /^[A-Z]/.test(name)
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/**
|
|
1382
|
+
* Check if a function body contains only simple constructor statements.
|
|
1383
|
+
* @param {import('jscodeshift').BlockStatement} functionBody - The function body to check.
|
|
1384
|
+
* @returns {boolean} True if the body contains only allowed statements.
|
|
1385
|
+
*/
|
|
1386
|
+
function hasSimpleConstructorBody(functionBody) {
|
|
1387
|
+
if (functionBody.body.length === 0) {
|
|
1388
|
+
return true
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return functionBody.body.every((statement) => {
|
|
1392
|
+
if (j.VariableDeclaration.check(statement)) {
|
|
1393
|
+
return true
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (j.ExpressionStatement.check(statement)) {
|
|
1397
|
+
return true
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
return false
|
|
1401
|
+
})
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
/**
|
|
1405
|
+
* Find all constructor functions in the AST.
|
|
1406
|
+
* @param {import('jscodeshift').JSCodeshift} j - The jscodeshift API.
|
|
1407
|
+
* @param {import('jscodeshift').Collection} root - The root AST collection.
|
|
1408
|
+
* @returns {Map<string, { declaration: import('jscodeshift').ASTPath, prototypeMethods: Array<any> }>} Map of constructor names to their info.
|
|
1409
|
+
*/
|
|
1410
|
+
function findConstructors(j, root) {
|
|
1411
|
+
const constructors = new Map()
|
|
1412
|
+
|
|
1413
|
+
// Handle function declarations
|
|
1414
|
+
root.find(j.FunctionDeclaration).forEach((path) => {
|
|
1415
|
+
const node = path.node
|
|
1416
|
+
const functionName = node.id ? node.id.name : null
|
|
1417
|
+
|
|
1418
|
+
if (!functionName || !isConstructorName(functionName)) {
|
|
1419
|
+
return
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (!hasSimpleConstructorBody(node.body)) {
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
constructors.set(functionName, {
|
|
1427
|
+
declaration: path,
|
|
1428
|
+
prototypeMethods: [],
|
|
1429
|
+
})
|
|
1430
|
+
})
|
|
1431
|
+
|
|
1432
|
+
// Handle variable declarations with function expressions
|
|
1433
|
+
root.find(j.VariableDeclaration).forEach((path) => {
|
|
1434
|
+
path.node.declarations.forEach((declarator) => {
|
|
1435
|
+
const functionName = declarator.id.name
|
|
1436
|
+
|
|
1437
|
+
if (!isConstructorName(functionName)) {
|
|
1438
|
+
return
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
if (!j.FunctionExpression.check(declarator.init)) {
|
|
1442
|
+
return
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const functionExpr = declarator.init
|
|
1446
|
+
|
|
1447
|
+
if (!hasSimpleConstructorBody(functionExpr.body)) {
|
|
1448
|
+
return
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
constructors.set(functionName, {
|
|
1452
|
+
declaration: path,
|
|
1453
|
+
prototypeMethods: [],
|
|
1454
|
+
})
|
|
1455
|
+
})
|
|
1456
|
+
})
|
|
1457
|
+
|
|
1458
|
+
return constructors
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
/**
|
|
1462
|
+
* Find and associate prototype methods with constructors.
|
|
1463
|
+
* @param {import('jscodeshift').JSCodeshift} j - The jscodeshift API.
|
|
1464
|
+
* @param {import('jscodeshift').Collection} root - The root AST collection.
|
|
1465
|
+
* @param {Map<string, { declaration: import('jscodeshift').ASTPath, prototypeMethods: Array<any> }>} constructors - Map of constructors.
|
|
1466
|
+
*/
|
|
1467
|
+
function findPrototypeMethods(j, root, constructors) {
|
|
1468
|
+
// Pattern 1: ConstructorName.prototype.methodName = ...
|
|
1469
|
+
root
|
|
1470
|
+
.find(j.ExpressionStatement)
|
|
1471
|
+
.filter((path) => {
|
|
1472
|
+
const node = path.node
|
|
1473
|
+
if (!j.AssignmentExpression.check(node.expression)) {
|
|
1474
|
+
return false
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const assignment = node.expression
|
|
1478
|
+
const left = assignment.left
|
|
1479
|
+
|
|
1480
|
+
if (
|
|
1481
|
+
!j.MemberExpression.check(left) ||
|
|
1482
|
+
!j.MemberExpression.check(left.object) ||
|
|
1483
|
+
!j.Identifier.check(left.object.object) ||
|
|
1484
|
+
!j.Identifier.check(left.object.property) ||
|
|
1485
|
+
left.object.property.name !== "prototype" ||
|
|
1486
|
+
!j.Identifier.check(left.property)
|
|
1487
|
+
) {
|
|
1488
|
+
return false
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const constructorName = left.object.object.name
|
|
1492
|
+
return constructors.has(constructorName)
|
|
1493
|
+
})
|
|
1494
|
+
.forEach((path) => {
|
|
1495
|
+
const assignment = path.node.expression
|
|
1496
|
+
const left = assignment.left
|
|
1497
|
+
const constructorName = left.object.object.name
|
|
1498
|
+
const methodName = left.property.name
|
|
1499
|
+
const methodValue = assignment.right
|
|
1500
|
+
|
|
1501
|
+
if (!j.FunctionExpression.check(methodValue)) {
|
|
1502
|
+
return
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
constructors.get(constructorName).prototypeMethods.push({
|
|
1506
|
+
path,
|
|
1507
|
+
methodName,
|
|
1508
|
+
methodValue,
|
|
1509
|
+
})
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
// Pattern 2: ConstructorName.prototype = { methodName: function() {...}, ... }
|
|
1513
|
+
root
|
|
1514
|
+
.find(j.ExpressionStatement)
|
|
1515
|
+
.filter((path) => {
|
|
1516
|
+
const node = path.node
|
|
1517
|
+
if (!j.AssignmentExpression.check(node.expression)) {
|
|
1518
|
+
return false
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
const assignment = node.expression
|
|
1522
|
+
const left = assignment.left
|
|
1523
|
+
|
|
1524
|
+
if (
|
|
1525
|
+
!j.MemberExpression.check(left) ||
|
|
1526
|
+
!j.Identifier.check(left.object) ||
|
|
1527
|
+
!j.Identifier.check(left.property) ||
|
|
1528
|
+
left.property.name !== "prototype"
|
|
1529
|
+
) {
|
|
1530
|
+
return false
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
const constructorName = left.object.name
|
|
1534
|
+
return constructors.has(constructorName)
|
|
1535
|
+
})
|
|
1536
|
+
.forEach((path) => {
|
|
1537
|
+
const assignment = path.node.expression
|
|
1538
|
+
const left = assignment.left
|
|
1539
|
+
const constructorName = left.object.name
|
|
1540
|
+
const methodValue = assignment.right
|
|
1541
|
+
|
|
1542
|
+
if (!j.ObjectExpression.check(methodValue)) {
|
|
1543
|
+
return
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
methodValue.properties.forEach((prop) => {
|
|
1547
|
+
if (!j.Property.check(prop) && !j.ObjectProperty.check(prop)) {
|
|
1548
|
+
return
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
if (prop.computed) {
|
|
1552
|
+
return
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
let methodName
|
|
1556
|
+
if (j.Identifier.check(prop.key)) {
|
|
1557
|
+
methodName = prop.key.name
|
|
1558
|
+
} else {
|
|
1559
|
+
return
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
if (!j.FunctionExpression.check(prop.value)) {
|
|
1563
|
+
return
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
constructors.get(constructorName).prototypeMethods.push({
|
|
1567
|
+
path,
|
|
1568
|
+
methodName,
|
|
1569
|
+
methodValue: prop.value,
|
|
1570
|
+
isObjectLiteral: true,
|
|
1571
|
+
})
|
|
1572
|
+
})
|
|
1573
|
+
})
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
/**
|
|
1577
|
+
* Transform constructors and their prototype methods to class syntax.
|
|
1578
|
+
* @param {import('jscodeshift').JSCodeshift} j - The jscodeshift API.
|
|
1579
|
+
* @param {import('jscodeshift').Collection} root - The root AST collection.
|
|
1580
|
+
* @param {Map<string, { declaration: import('jscodeshift').ASTPath, prototypeMethods: Array<any> }>} constructors - Map of constructors.
|
|
1581
|
+
* @returns {{ modified: boolean, changes: Array<{ type: string, line: number }> }} Transformation result.
|
|
1582
|
+
*/
|
|
1583
|
+
function transformConstructorsToClasses(j, root, constructors) {
|
|
1584
|
+
let modified = false
|
|
1585
|
+
const changes = []
|
|
1586
|
+
|
|
1587
|
+
constructors.forEach((info, constructorName) => {
|
|
1588
|
+
if (info.prototypeMethods.length === 0) {
|
|
1589
|
+
return
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
const declarationPath = info.declaration
|
|
1593
|
+
const declarationNode = declarationPath.node
|
|
1594
|
+
|
|
1595
|
+
let constructorNode
|
|
1596
|
+
if (j.FunctionDeclaration.check(declarationNode)) {
|
|
1597
|
+
constructorNode = declarationNode
|
|
1598
|
+
} else {
|
|
1599
|
+
const declarator = declarationNode.declarations.find(
|
|
1600
|
+
(decl) => decl.id.name === constructorName,
|
|
1601
|
+
)
|
|
1602
|
+
constructorNode = declarator.init
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const classBody = []
|
|
1606
|
+
|
|
1607
|
+
const constructorMethod = j.methodDefinition(
|
|
1608
|
+
"constructor",
|
|
1609
|
+
j.identifier("constructor"),
|
|
1610
|
+
j.functionExpression(
|
|
1611
|
+
null,
|
|
1612
|
+
constructorNode.params,
|
|
1613
|
+
constructorNode.body,
|
|
1614
|
+
constructorNode.generator,
|
|
1615
|
+
constructorNode.async,
|
|
1616
|
+
),
|
|
1617
|
+
false,
|
|
1618
|
+
)
|
|
1619
|
+
classBody.push(constructorMethod)
|
|
1620
|
+
|
|
1621
|
+
info.prototypeMethods.forEach(({ methodName, methodValue }) => {
|
|
1622
|
+
const method = j.methodDefinition(
|
|
1623
|
+
"method",
|
|
1624
|
+
j.identifier(methodName),
|
|
1625
|
+
j.functionExpression(
|
|
1626
|
+
null,
|
|
1627
|
+
methodValue.params,
|
|
1628
|
+
methodValue.body,
|
|
1629
|
+
methodValue.generator,
|
|
1630
|
+
methodValue.async,
|
|
1631
|
+
),
|
|
1632
|
+
false,
|
|
1633
|
+
)
|
|
1634
|
+
classBody.push(method)
|
|
1635
|
+
})
|
|
1636
|
+
|
|
1637
|
+
const classDeclaration = j.classDeclaration(
|
|
1638
|
+
j.identifier(constructorName),
|
|
1639
|
+
j.classBody(classBody),
|
|
1640
|
+
)
|
|
1641
|
+
|
|
1642
|
+
j(info.declaration).replaceWith(classDeclaration)
|
|
1643
|
+
|
|
1644
|
+
const pathsToRemove = new Set()
|
|
1645
|
+
info.prototypeMethods.forEach(({ path }) => {
|
|
1646
|
+
pathsToRemove.add(path)
|
|
1647
|
+
})
|
|
1648
|
+
|
|
1649
|
+
pathsToRemove.forEach((path) => {
|
|
1650
|
+
j(path).remove()
|
|
1651
|
+
})
|
|
1652
|
+
|
|
1653
|
+
modified = true
|
|
1654
|
+
if (constructorNode.loc) {
|
|
1655
|
+
changes.push({
|
|
1656
|
+
type: "constructorToClass",
|
|
1657
|
+
line: constructorNode.loc.start.line,
|
|
1658
|
+
})
|
|
1659
|
+
}
|
|
1660
|
+
})
|
|
1661
|
+
|
|
1662
|
+
return { modified, changes }
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const constructors = findConstructors(j, root)
|
|
1666
|
+
findPrototypeMethods(j, root, constructors)
|
|
1667
|
+
return transformConstructorsToClasses(j, root, constructors)
|
|
1668
|
+
}
|
|
@@ -2175,4 +2175,579 @@ const result = [1, 2].concat(other);`)
|
|
|
2175
2175
|
assert.match(result.code, /`Hello \$\{userName\}`/)
|
|
2176
2176
|
})
|
|
2177
2177
|
})
|
|
2178
|
+
|
|
2179
|
+
describe("constructorToClass", () => {
|
|
2180
|
+
test("simple constructor with prototype methods", () => {
|
|
2181
|
+
const result = transform(`
|
|
2182
|
+
function Person(name) {
|
|
2183
|
+
this.name = name;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
Person.prototype.greet = function() {
|
|
2187
|
+
return 'Hello, ' + this.name;
|
|
2188
|
+
};
|
|
2189
|
+
`)
|
|
2190
|
+
|
|
2191
|
+
assert(result.modified, "transform constructor to class")
|
|
2192
|
+
assert.match(result.code, /class Person/)
|
|
2193
|
+
assert.match(result.code, /constructor\(name\)/)
|
|
2194
|
+
assert.match(result.code, /greet\(\)/)
|
|
2195
|
+
})
|
|
2196
|
+
|
|
2197
|
+
test("constructor with multiple prototype methods", () => {
|
|
2198
|
+
const result = transform(`
|
|
2199
|
+
function Animal(type) {
|
|
2200
|
+
this.type = type;
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
Animal.prototype.speak = function() {
|
|
2204
|
+
return this.type + ' makes a sound';
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
Animal.prototype.move = function() {
|
|
2208
|
+
return this.type + ' is moving';
|
|
2209
|
+
};
|
|
2210
|
+
`)
|
|
2211
|
+
|
|
2212
|
+
assert(result.modified, "transform constructor with multiple methods")
|
|
2213
|
+
assert.match(result.code, /class Animal/)
|
|
2214
|
+
assert.match(result.code, /speak\(\)/)
|
|
2215
|
+
assert.match(result.code, /move\(\)/)
|
|
2216
|
+
})
|
|
2217
|
+
|
|
2218
|
+
test("skip lowercase function names", () => {
|
|
2219
|
+
const result = transform(`
|
|
2220
|
+
function helper(value) {
|
|
2221
|
+
this.value = value;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
helper.prototype.process = function() {
|
|
2225
|
+
return this.value * 2;
|
|
2226
|
+
};
|
|
2227
|
+
`)
|
|
2228
|
+
|
|
2229
|
+
assert(!result.modified, "skip lowercase function names")
|
|
2230
|
+
assert.match(result.code, /function helper/)
|
|
2231
|
+
})
|
|
2232
|
+
|
|
2233
|
+
test("skip constructor without prototype methods", () => {
|
|
2234
|
+
const result = transform(`
|
|
2235
|
+
function Person(name) {
|
|
2236
|
+
this.name = name;
|
|
2237
|
+
}
|
|
2238
|
+
`)
|
|
2239
|
+
|
|
2240
|
+
assert(!result.modified, "skip constructor without prototype methods")
|
|
2241
|
+
assert.match(result.code, /function Person/)
|
|
2242
|
+
})
|
|
2243
|
+
|
|
2244
|
+
test("skip if not all methods are function expressions", () => {
|
|
2245
|
+
const result = transform(`
|
|
2246
|
+
function Person(name) {
|
|
2247
|
+
this.name = name;
|
|
2248
|
+
}
|
|
2249
|
+
|
|
2250
|
+
Person.prototype.greet = () => {
|
|
2251
|
+
return 'Hello';
|
|
2252
|
+
};
|
|
2253
|
+
`)
|
|
2254
|
+
|
|
2255
|
+
assert(!result.modified, "skip arrow functions on prototype")
|
|
2256
|
+
assert.match(result.code, /function Person/)
|
|
2257
|
+
})
|
|
2258
|
+
|
|
2259
|
+
test("constructor with no parameters", () => {
|
|
2260
|
+
const result = transform(`
|
|
2261
|
+
function Counter() {
|
|
2262
|
+
this.count = 0;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
Counter.prototype.increment = function() {
|
|
2266
|
+
this.count++;
|
|
2267
|
+
};
|
|
2268
|
+
`)
|
|
2269
|
+
|
|
2270
|
+
assert(result.modified, "transform constructor with no parameters")
|
|
2271
|
+
assert.match(result.code, /class Counter/)
|
|
2272
|
+
assert.match(result.code, /constructor\(\)/)
|
|
2273
|
+
assert.match(result.code, /increment\(\)/)
|
|
2274
|
+
})
|
|
2275
|
+
|
|
2276
|
+
test("transform constructor with variable declarations in body", () => {
|
|
2277
|
+
const result = transform(`
|
|
2278
|
+
function Person(name) {
|
|
2279
|
+
this.name = name;
|
|
2280
|
+
const temp = processName(name);
|
|
2281
|
+
this.processed = temp;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
Person.prototype.greet = function() {
|
|
2285
|
+
return 'Hello, I am ' + this.name;
|
|
2286
|
+
};
|
|
2287
|
+
`)
|
|
2288
|
+
|
|
2289
|
+
// The constructor can be safely transformed even with variable declarations
|
|
2290
|
+
assert.match(result.code, /class Person/)
|
|
2291
|
+
assert.match(result.code, /constructor\(name\)/)
|
|
2292
|
+
assert.match(result.code, /greet\(\)/)
|
|
2293
|
+
})
|
|
2294
|
+
|
|
2295
|
+
test("constructor with expression body statements", () => {
|
|
2296
|
+
const result = transform(`
|
|
2297
|
+
function Person(name, age) {
|
|
2298
|
+
this.name = name;
|
|
2299
|
+
this.age = age;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
Person.prototype.greet = function() {
|
|
2303
|
+
return 'Hello, ' + this.name;
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
Person.prototype.getAge = function() {
|
|
2307
|
+
return this.age;
|
|
2308
|
+
};
|
|
2309
|
+
`)
|
|
2310
|
+
|
|
2311
|
+
assert(result.modified, "transform constructor with multiple properties")
|
|
2312
|
+
assert.match(result.code, /class Person/)
|
|
2313
|
+
assert.match(result.code, /constructor\(name, age\)/)
|
|
2314
|
+
})
|
|
2315
|
+
|
|
2316
|
+
test("skip factory pattern with return statement", () => {
|
|
2317
|
+
const result = transform(`
|
|
2318
|
+
function Something() {
|
|
2319
|
+
var depth = 0;
|
|
2320
|
+
return {
|
|
2321
|
+
incDepth: function() {
|
|
2322
|
+
depth++;
|
|
2323
|
+
}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
foo = Something();
|
|
2328
|
+
`)
|
|
2329
|
+
|
|
2330
|
+
// Factory pattern should not be transformed to a class
|
|
2331
|
+
assert.match(result.code, /function Something/)
|
|
2332
|
+
assert.doesNotMatch(result.code, /class Something/)
|
|
2333
|
+
})
|
|
2334
|
+
|
|
2335
|
+
test("skip constructor with return statement", () => {
|
|
2336
|
+
const result = transform(`
|
|
2337
|
+
function Factory(config) {
|
|
2338
|
+
this.config = config;
|
|
2339
|
+
return this.config;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
Factory.prototype.process = function() {
|
|
2343
|
+
return this.config;
|
|
2344
|
+
};
|
|
2345
|
+
`)
|
|
2346
|
+
|
|
2347
|
+
// Constructor with return statement should not be transformed
|
|
2348
|
+
assert.match(result.code, /function Factory/)
|
|
2349
|
+
assert.doesNotMatch(result.code, /class Factory/)
|
|
2350
|
+
})
|
|
2351
|
+
|
|
2352
|
+
test("skip function expression with method call in constructor", () => {
|
|
2353
|
+
const result = transform(`
|
|
2354
|
+
var SomeClass = function (selector) {
|
|
2355
|
+
this.element = document.querySelector(selector);
|
|
2356
|
+
this.init();
|
|
2357
|
+
};
|
|
2358
|
+
|
|
2359
|
+
SomeClass.prototype = {
|
|
2360
|
+
init: function () {
|
|
2361
|
+
console.log('init');
|
|
2362
|
+
}
|
|
2363
|
+
};
|
|
2364
|
+
`)
|
|
2365
|
+
|
|
2366
|
+
// Constructor with method call should not be transformed (not a simple constructor)
|
|
2367
|
+
assert.doesNotMatch(result.code, /class SomeClass/)
|
|
2368
|
+
})
|
|
2369
|
+
|
|
2370
|
+
test("skip prototype assignment with non-function methods", () => {
|
|
2371
|
+
const result = transform(`
|
|
2372
|
+
function Widget(id) {
|
|
2373
|
+
this.id = id;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
Widget.prototype = {
|
|
2377
|
+
value: 42
|
|
2378
|
+
};
|
|
2379
|
+
`)
|
|
2380
|
+
|
|
2381
|
+
assert(!result.modified, "skip prototype object without function expressions")
|
|
2382
|
+
assert.match(result.code, /function Widget/)
|
|
2383
|
+
})
|
|
2384
|
+
|
|
2385
|
+
test("skip prototype assignment with arrow function methods", () => {
|
|
2386
|
+
const result = transform(`
|
|
2387
|
+
function Component(props) {
|
|
2388
|
+
this.props = props;
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
Component.prototype = {
|
|
2392
|
+
render: () => {
|
|
2393
|
+
return null;
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
`)
|
|
2397
|
+
|
|
2398
|
+
assert(!result.modified, "skip prototype object with arrow functions")
|
|
2399
|
+
assert.match(result.code, /function Component/)
|
|
2400
|
+
})
|
|
2401
|
+
|
|
2402
|
+
test("skip prototype assignment with getter/setter properties", () => {
|
|
2403
|
+
const result = transform(`
|
|
2404
|
+
function Model(name) {
|
|
2405
|
+
this.name = name;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
Model.prototype = {
|
|
2409
|
+
get value() {
|
|
2410
|
+
return this.name;
|
|
2411
|
+
}
|
|
2412
|
+
};
|
|
2413
|
+
`)
|
|
2414
|
+
|
|
2415
|
+
assert(!result.modified, "skip prototype object with getter properties")
|
|
2416
|
+
assert.match(result.code, /function Model/)
|
|
2417
|
+
})
|
|
2418
|
+
|
|
2419
|
+
test("skip prototype assignment with computed properties", () => {
|
|
2420
|
+
const result = transform(`
|
|
2421
|
+
function Handler(type) {
|
|
2422
|
+
this.type = type;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
Handler.prototype = {
|
|
2426
|
+
[Symbol.toStringTag]: function() {
|
|
2427
|
+
return this.type;
|
|
2428
|
+
}
|
|
2429
|
+
};
|
|
2430
|
+
`)
|
|
2431
|
+
|
|
2432
|
+
assert(!result.modified, "skip prototype object with computed properties")
|
|
2433
|
+
assert.match(result.code, /function Handler/)
|
|
2434
|
+
})
|
|
2435
|
+
|
|
2436
|
+
test("skip constructor with complex body statements", () => {
|
|
2437
|
+
const result = transform(`
|
|
2438
|
+
function Service(config) {
|
|
2439
|
+
if (config) {
|
|
2440
|
+
this.config = config;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
Service.prototype.init = function() {
|
|
2445
|
+
return this.config;
|
|
2446
|
+
};
|
|
2447
|
+
`)
|
|
2448
|
+
|
|
2449
|
+
assert(!result.modified, "skip constructor with if statement")
|
|
2450
|
+
assert.match(result.code, /function Service/)
|
|
2451
|
+
})
|
|
2452
|
+
|
|
2453
|
+
test("skip prototype method assignment with non-function value", () => {
|
|
2454
|
+
const result = transform(`
|
|
2455
|
+
function Manager(id) {
|
|
2456
|
+
this.id = id;
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
Manager.prototype.defaultValue = 10;
|
|
2460
|
+
`)
|
|
2461
|
+
|
|
2462
|
+
assert(!result.modified, "skip prototype with non-function assignment")
|
|
2463
|
+
assert.match(result.code, /function Manager/)
|
|
2464
|
+
})
|
|
2465
|
+
|
|
2466
|
+
test("variable declaration constructor with generator method", () => {
|
|
2467
|
+
const result = transform(`
|
|
2468
|
+
var Generator = function(data) {
|
|
2469
|
+
this.data = data;
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
Generator.prototype.process = function*() {
|
|
2473
|
+
yield this.data;
|
|
2474
|
+
};
|
|
2475
|
+
`)
|
|
2476
|
+
|
|
2477
|
+
assert(result.modified, "transform constructor with generator method")
|
|
2478
|
+
assert.match(result.code, /class Generator/)
|
|
2479
|
+
assert.match(result.code, /\*process/)
|
|
2480
|
+
})
|
|
2481
|
+
|
|
2482
|
+
test("skip pattern 2 assignment with non-object value", () => {
|
|
2483
|
+
const result = transform(`
|
|
2484
|
+
function Parser(input) {
|
|
2485
|
+
this.input = input;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
Parser.prototype = null;
|
|
2489
|
+
`)
|
|
2490
|
+
|
|
2491
|
+
assert(!result.modified, "skip prototype assignment to non-object")
|
|
2492
|
+
assert.match(result.code, /function Parser/)
|
|
2493
|
+
})
|
|
2494
|
+
|
|
2495
|
+
test("constructor with block statement in body", () => {
|
|
2496
|
+
const result = transform(`
|
|
2497
|
+
function Strict(mode) {
|
|
2498
|
+
'use strict';
|
|
2499
|
+
this.mode = mode;
|
|
2500
|
+
}
|
|
2501
|
+
|
|
2502
|
+
Strict.prototype.check = function() {
|
|
2503
|
+
return this.mode;
|
|
2504
|
+
};
|
|
2505
|
+
`)
|
|
2506
|
+
|
|
2507
|
+
assert(result.modified, "transform constructor with directive")
|
|
2508
|
+
assert.match(result.code, /class Strict/)
|
|
2509
|
+
})
|
|
2510
|
+
|
|
2511
|
+
test("variable declaration constructor with prototype methods", () => {
|
|
2512
|
+
const result = transform(`
|
|
2513
|
+
var Calculator = function(value) {
|
|
2514
|
+
this.value = value;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
Calculator.prototype.add = function(num) {
|
|
2518
|
+
return this.value + num;
|
|
2519
|
+
};
|
|
2520
|
+
`)
|
|
2521
|
+
|
|
2522
|
+
assert(result.modified, "transform variable declaration constructor")
|
|
2523
|
+
assert.match(result.code, /class Calculator/)
|
|
2524
|
+
})
|
|
2525
|
+
|
|
2526
|
+
test("skip constructor with throw statement in body", () => {
|
|
2527
|
+
const result = transform(`
|
|
2528
|
+
function Validator(value) {
|
|
2529
|
+
if (!value) {
|
|
2530
|
+
throw new Error('invalid');
|
|
2531
|
+
}
|
|
2532
|
+
this.value = value;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
Validator.prototype.validate = function() {
|
|
2536
|
+
return this.value;
|
|
2537
|
+
};
|
|
2538
|
+
`)
|
|
2539
|
+
|
|
2540
|
+
assert(!result.modified, "skip constructor with throw")
|
|
2541
|
+
assert.match(result.code, /function Validator/)
|
|
2542
|
+
})
|
|
2543
|
+
|
|
2544
|
+
test("transform constructor with only assignment statements", () => {
|
|
2545
|
+
const result = transform(`
|
|
2546
|
+
function Iterator(items) {
|
|
2547
|
+
this.items = items;
|
|
2548
|
+
this.index = 0;
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2551
|
+
Iterator.prototype.next = function() {
|
|
2552
|
+
return this.items[this.index++];
|
|
2553
|
+
};
|
|
2554
|
+
`)
|
|
2555
|
+
|
|
2556
|
+
assert(result.modified, "transform constructor with safe body")
|
|
2557
|
+
assert.match(result.code, /class Iterator/)
|
|
2558
|
+
})
|
|
2559
|
+
|
|
2560
|
+
test("skip constructor with computed property in prototype literal", () => {
|
|
2561
|
+
const result = transform(`
|
|
2562
|
+
function Handler(type) {
|
|
2563
|
+
this.type = type;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
Handler.prototype = {
|
|
2567
|
+
[Symbol.toStringTag]: function() {
|
|
2568
|
+
return this.type;
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
`)
|
|
2572
|
+
|
|
2573
|
+
assert(!result.modified, "skip prototype with computed property")
|
|
2574
|
+
assert.match(result.code, /function Handler/)
|
|
2575
|
+
})
|
|
2576
|
+
|
|
2577
|
+
test("prototype literal with non-identifier properties", () => {
|
|
2578
|
+
const result = transform(`
|
|
2579
|
+
function Component(props) {
|
|
2580
|
+
this.props = props;
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
Component.prototype = {
|
|
2584
|
+
'method-name': function() {
|
|
2585
|
+
return this.props;
|
|
2586
|
+
}
|
|
2587
|
+
};
|
|
2588
|
+
`)
|
|
2589
|
+
|
|
2590
|
+
assert(!result.modified, "skip prototype literal with string keys")
|
|
2591
|
+
assert.match(result.code, /function Component/)
|
|
2592
|
+
})
|
|
2593
|
+
|
|
2594
|
+
test("constructor with return statement prevents transformation", () => {
|
|
2595
|
+
const result = transform(`
|
|
2596
|
+
function Creator(config) {
|
|
2597
|
+
this.config = config;
|
|
2598
|
+
if (!config) {
|
|
2599
|
+
return;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
Creator.prototype.start = function() {
|
|
2604
|
+
return this.config;
|
|
2605
|
+
};
|
|
2606
|
+
`)
|
|
2607
|
+
|
|
2608
|
+
assert(!result.modified, "skip constructor with return")
|
|
2609
|
+
assert.match(result.code, /function Creator/)
|
|
2610
|
+
})
|
|
2611
|
+
|
|
2612
|
+
test("prototype object with only getter property", () => {
|
|
2613
|
+
const result = transform(`
|
|
2614
|
+
function Box(value) {
|
|
2615
|
+
this.value = value;
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2618
|
+
Box.prototype = {
|
|
2619
|
+
get size() {
|
|
2620
|
+
return this.value * 2;
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
`)
|
|
2624
|
+
|
|
2625
|
+
assert(!result.modified, "skip constructor when only getter in prototype")
|
|
2626
|
+
assert.match(result.code, /function Box/)
|
|
2627
|
+
})
|
|
2628
|
+
|
|
2629
|
+
test("prototype object with getter and function method", () => {
|
|
2630
|
+
const result = transform(`
|
|
2631
|
+
function Box(value) {
|
|
2632
|
+
this.value = value;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
Box.prototype = {
|
|
2636
|
+
get size() {
|
|
2637
|
+
return this.value * 2;
|
|
2638
|
+
},
|
|
2639
|
+
getValue: function() {
|
|
2640
|
+
return this.value;
|
|
2641
|
+
}
|
|
2642
|
+
};
|
|
2643
|
+
`)
|
|
2644
|
+
|
|
2645
|
+
assert(result.modified, "transform constructor ignoring getter property")
|
|
2646
|
+
assert.match(result.code, /class Box/)
|
|
2647
|
+
})
|
|
2648
|
+
|
|
2649
|
+
test("prototype object with only non-function property", () => {
|
|
2650
|
+
const result = transform(`
|
|
2651
|
+
function Widget(id) {
|
|
2652
|
+
this.id = id;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
Widget.prototype = {
|
|
2656
|
+
config: { timeout: 1000 }
|
|
2657
|
+
};
|
|
2658
|
+
`)
|
|
2659
|
+
|
|
2660
|
+
assert(!result.modified, "skip constructor when no function methods")
|
|
2661
|
+
assert.match(result.code, /function Widget/)
|
|
2662
|
+
})
|
|
2663
|
+
|
|
2664
|
+
test("prototype object with non-function and function properties", () => {
|
|
2665
|
+
const result = transform(`
|
|
2666
|
+
function Widget(id) {
|
|
2667
|
+
this.id = id;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
Widget.prototype = {
|
|
2671
|
+
config: { timeout: 1000 },
|
|
2672
|
+
getValue: function() {
|
|
2673
|
+
return this.id;
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
`)
|
|
2677
|
+
|
|
2678
|
+
assert(result.modified, "transform constructor ignoring non-function property")
|
|
2679
|
+
assert.match(result.code, /class Widget/)
|
|
2680
|
+
})
|
|
2681
|
+
|
|
2682
|
+
test("skip var declaration with arrow function", () => {
|
|
2683
|
+
const result = transform(`
|
|
2684
|
+
var Calculator = (value) => {
|
|
2685
|
+
this.value = value;
|
|
2686
|
+
};
|
|
2687
|
+
|
|
2688
|
+
Calculator.prototype.add = function(num) {
|
|
2689
|
+
return this.value + num;
|
|
2690
|
+
};
|
|
2691
|
+
`)
|
|
2692
|
+
|
|
2693
|
+
assert.doesNotMatch(
|
|
2694
|
+
result.code,
|
|
2695
|
+
/class Calculator/,
|
|
2696
|
+
"skip arrow function constructor",
|
|
2697
|
+
)
|
|
2698
|
+
})
|
|
2699
|
+
|
|
2700
|
+
test("skip var declaration constructor with complex body", () => {
|
|
2701
|
+
const result = transform(`
|
|
2702
|
+
var Service = function(config) {
|
|
2703
|
+
if (config) {
|
|
2704
|
+
this.config = config;
|
|
2705
|
+
}
|
|
2706
|
+
};
|
|
2707
|
+
|
|
2708
|
+
Service.prototype.init = function() {
|
|
2709
|
+
return this.config;
|
|
2710
|
+
};
|
|
2711
|
+
`)
|
|
2712
|
+
|
|
2713
|
+
assert.doesNotMatch(
|
|
2714
|
+
result.code,
|
|
2715
|
+
/class Service/,
|
|
2716
|
+
"skip var declaration with complex body",
|
|
2717
|
+
)
|
|
2718
|
+
})
|
|
2719
|
+
|
|
2720
|
+
test("var declaration prototype with getter property", () => {
|
|
2721
|
+
const result = transform(`
|
|
2722
|
+
var Box = function(value) {
|
|
2723
|
+
this.value = value;
|
|
2724
|
+
};
|
|
2725
|
+
|
|
2726
|
+
Box.prototype = {
|
|
2727
|
+
get size() {
|
|
2728
|
+
return this.value * 2;
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
`)
|
|
2732
|
+
|
|
2733
|
+
assert.doesNotMatch(
|
|
2734
|
+
result.code,
|
|
2735
|
+
/class Box/,
|
|
2736
|
+
"skip var declaration with getter prototype",
|
|
2737
|
+
)
|
|
2738
|
+
})
|
|
2739
|
+
|
|
2740
|
+
test("function declaration constructor with empty body", () => {
|
|
2741
|
+
const result = transform(`
|
|
2742
|
+
function Empty() {}
|
|
2743
|
+
|
|
2744
|
+
Empty.prototype.run = function() {
|
|
2745
|
+
return this.value;
|
|
2746
|
+
};
|
|
2747
|
+
`)
|
|
2748
|
+
|
|
2749
|
+
assert(result.modified, "transform function declaration with empty body")
|
|
2750
|
+
assert.match(result.code, /class Empty/)
|
|
2751
|
+
})
|
|
2752
|
+
})
|
|
2178
2753
|
})
|
package/AGENTS.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
When writing code, you MUST ALWAYS follow the [naming-things](https://raw.githubusercontent.com/codingjoe/naming-things/refs/heads/main/README.md) guidelines.
|
|
2
|
-
|
|
3
|
-
All code must be fully tested with a 100% coverage. Unreachable code must be removed.
|
|
4
|
-
|
|
5
|
-
All transformers must be documented in the README.md.
|