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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esupgrade",
3
- "version": "2025.3.4",
3
+ "version": "2025.4.0",
4
4
  "description": "Auto-upgrade your JavaScript syntax",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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.